diff --git a/test/token/ERC721/ERC721.behavior.js b/test/token/ERC721/ERC721.behavior.js index 10f84826547..32d67d90d98 100644 --- a/test/token/ERC721/ERC721.behavior.js +++ b/test/token/ERC721/ERC721.behavior.js @@ -1,68 +1,76 @@ -const { BN, constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); +const { ethers } = require('hardhat'); const { expect } = require('chai'); -const { ZERO_ADDRESS } = constants; +const { PANIC_CODES } = require('@nomicfoundation/hardhat-chai-matchers/panic'); +const { anyValue } = require('@nomicfoundation/hardhat-chai-matchers/withArgs'); const { shouldSupportInterfaces } = require('../../utils/introspection/SupportsInterface.behavior'); -const { expectRevertCustomError } = require('../../helpers/customError'); -const { Enum } = require('../../helpers/enums'); - -const ERC721ReceiverMock = artifacts.require('ERC721ReceiverMock'); -const NonERC721ReceiverMock = artifacts.require('CallReceiverMock'); +const { + bigint: { Enum }, +} = require('../../helpers/enums'); const RevertType = Enum('None', 'RevertWithoutMessage', 'RevertWithMessage', 'RevertWithCustomError', 'Panic'); -const firstTokenId = new BN('5042'); -const secondTokenId = new BN('79217'); -const nonExistentTokenId = new BN('13'); -const fourthTokenId = new BN(4); +const firstTokenId = 5042n; +const secondTokenId = 79217n; +const nonExistentTokenId = 13n; +const fourthTokenId = 4n; const baseURI = 'https://api.example.com/v1/'; const RECEIVER_MAGIC_VALUE = '0x150b7a02'; -function shouldBehaveLikeERC721(owner, newOwner, approved, anotherApproved, operator, other) { +function shouldBehaveLikeERC721() { + beforeEach(async function () { + const [owner, newOwner, approved, operator, other] = this.accounts; + Object.assign(this, { owner, newOwner, approved, operator, other }); + }); + shouldSupportInterfaces(['ERC165', 'ERC721']); - context('with minted tokens', function () { + describe('with minted tokens', function () { beforeEach(async function () { - await this.token.$_mint(owner, firstTokenId); - await this.token.$_mint(owner, secondTokenId); - this.toWhom = other; // default to other for toWhom in context-dependent tests + await this.token.$_mint(this.owner, firstTokenId); + await this.token.$_mint(this.owner, secondTokenId); + this.to = this.other; }); describe('balanceOf', function () { - context('when the given address owns some tokens', function () { + describe('when the given address owns some tokens', function () { it('returns the amount of tokens owned by the given address', async function () { - expect(await this.token.balanceOf(owner)).to.be.bignumber.equal('2'); + expect(await this.token.balanceOf(this.owner)).to.equal(2n); }); }); - context('when the given address does not own any tokens', function () { + describe('when the given address does not own any tokens', function () { it('returns 0', async function () { - expect(await this.token.balanceOf(other)).to.be.bignumber.equal('0'); + expect(await this.token.balanceOf(this.other)).to.equal(0n); }); }); - context('when querying the zero address', function () { + describe('when querying the zero address', function () { it('throws', async function () { - await expectRevertCustomError(this.token.balanceOf(ZERO_ADDRESS), 'ERC721InvalidOwner', [ZERO_ADDRESS]); + await expect(this.token.balanceOf(ethers.ZeroAddress)) + .to.be.revertedWithCustomError(this.token, 'ERC721InvalidOwner') + .withArgs(ethers.ZeroAddress); }); }); }); describe('ownerOf', function () { - context('when the given token ID was tracked by this token', function () { + describe('when the given token ID was tracked by this token', function () { const tokenId = firstTokenId; it('returns the owner of the given token ID', async function () { - expect(await this.token.ownerOf(tokenId)).to.be.equal(owner); + expect(await this.token.ownerOf(tokenId)).to.equal(this.owner.address); }); }); - context('when the given token ID was not tracked by this token', function () { + describe('when the given token ID was not tracked by this token', function () { const tokenId = nonExistentTokenId; it('reverts', async function () { - await expectRevertCustomError(this.token.ownerOf(tokenId), 'ERC721NonexistentToken', [tokenId]); + await expect(this.token.ownerOf(tokenId)) + .to.be.revertedWithCustomError(this.token, 'ERC721NonexistentToken') + .withArgs(tokenId); }); }); }); @@ -71,194 +79,204 @@ function shouldBehaveLikeERC721(owner, newOwner, approved, anotherApproved, oper const tokenId = firstTokenId; const data = '0x42'; - let receipt = null; - beforeEach(async function () { - await this.token.approve(approved, tokenId, { from: owner }); - await this.token.setApprovalForAll(operator, true, { from: owner }); + await this.token.connect(this.owner).approve(this.approved, tokenId); + await this.token.connect(this.owner).setApprovalForAll(this.operator, true); }); - const transferWasSuccessful = function ({ owner, tokenId }) { + const transferWasSuccessful = () => { it('transfers the ownership of the given token ID to the given address', async function () { - expect(await this.token.ownerOf(tokenId)).to.be.equal(this.toWhom); + await this.tx(); + expect(await this.token.ownerOf(tokenId)).to.equal(this.to.address ?? this.to.target); }); it('emits a Transfer event', async function () { - expectEvent(receipt, 'Transfer', { from: owner, to: this.toWhom, tokenId: tokenId }); + await expect(this.tx()) + .to.emit(this.token, 'Transfer') + .withArgs(this.owner.address, this.to.address ?? this.to.target, tokenId); }); it('clears the approval for the token ID with no event', async function () { - expect(await this.token.getApproved(tokenId)).to.be.equal(ZERO_ADDRESS); - expectEvent.notEmitted(receipt, 'Approval'); + await expect(this.tx()).to.not.emit(this.token, 'Approval'); + + expect(await this.token.getApproved(tokenId)).to.equal(ethers.ZeroAddress); }); it('adjusts owners balances', async function () { - expect(await this.token.balanceOf(owner)).to.be.bignumber.equal('1'); + const balanceBefore = await this.token.balanceOf(this.owner); + await this.tx(); + expect(await this.token.balanceOf(this.owner)).to.equal(balanceBefore - 1n); }); it('adjusts owners tokens by index', async function () { if (!this.token.tokenOfOwnerByIndex) return; - expect(await this.token.tokenOfOwnerByIndex(this.toWhom, 0)).to.be.bignumber.equal(tokenId); - - expect(await this.token.tokenOfOwnerByIndex(owner, 0)).to.be.bignumber.not.equal(tokenId); + await this.tx(); + expect(await this.token.tokenOfOwnerByIndex(this.to, 0n)).to.equal(tokenId); + expect(await this.token.tokenOfOwnerByIndex(this.owner, 0n)).to.not.equal(tokenId); }); }; - const shouldTransferTokensByUsers = function (transferFunction, opts = {}) { - context('when called by the owner', function () { + const shouldTransferTokensByUsers = function (fragment, opts = {}) { + describe('when called by the owner', function () { beforeEach(async function () { - receipt = await transferFunction.call(this, owner, this.toWhom, tokenId, { from: owner }); + this.tx = () => + this.token.connect(this.owner)[fragment](this.owner, this.to, tokenId, ...(opts.extra ?? [])); }); - transferWasSuccessful({ owner, tokenId, approved }); + transferWasSuccessful(); }); - context('when called by the approved individual', function () { + describe('when called by the approved individual', function () { beforeEach(async function () { - receipt = await transferFunction.call(this, owner, this.toWhom, tokenId, { from: approved }); + this.tx = () => + this.token.connect(this.approved)[fragment](this.owner, this.to, tokenId, ...(opts.extra ?? [])); }); - transferWasSuccessful({ owner, tokenId, approved }); + transferWasSuccessful(); }); - context('when called by the operator', function () { + describe('when called by the operator', function () { beforeEach(async function () { - receipt = await transferFunction.call(this, owner, this.toWhom, tokenId, { from: operator }); + this.tx = () => + this.token.connect(this.operator)[fragment](this.owner, this.to, tokenId, ...(opts.extra ?? [])); }); - transferWasSuccessful({ owner, tokenId, approved }); + transferWasSuccessful(); }); - context('when called by the owner without an approved user', function () { + describe('when called by the owner without an approved user', function () { beforeEach(async function () { - await this.token.approve(ZERO_ADDRESS, tokenId, { from: owner }); - receipt = await transferFunction.call(this, owner, this.toWhom, tokenId, { from: operator }); + await this.token.connect(this.owner).approve(ethers.ZeroAddress, tokenId); + this.tx = () => + this.token.connect(this.operator)[fragment](this.owner, this.to, tokenId, ...(opts.extra ?? [])); }); - transferWasSuccessful({ owner, tokenId, approved: null }); + transferWasSuccessful(); }); - context('when sent to the owner', function () { + describe('when sent to the owner', function () { beforeEach(async function () { - receipt = await transferFunction.call(this, owner, owner, tokenId, { from: owner }); + this.tx = () => + this.token.connect(this.owner)[fragment](this.owner, this.owner, tokenId, ...(opts.extra ?? [])); }); it('keeps ownership of the token', async function () { - expect(await this.token.ownerOf(tokenId)).to.be.equal(owner); + await this.tx(); + expect(await this.token.ownerOf(tokenId)).to.equal(this.owner.address); }); it('clears the approval for the token ID', async function () { - expect(await this.token.getApproved(tokenId)).to.be.equal(ZERO_ADDRESS); + await this.tx(); + expect(await this.token.getApproved(tokenId)).to.equal(ethers.ZeroAddress); }); it('emits only a transfer event', async function () { - expectEvent(receipt, 'Transfer', { - from: owner, - to: owner, - tokenId: tokenId, - }); + await expect(this.tx()) + .to.emit(this.token, 'Transfer') + .withArgs(this.owner.address, this.owner.address, tokenId); }); it('keeps the owner balance', async function () { - expect(await this.token.balanceOf(owner)).to.be.bignumber.equal('2'); + const balanceBefore = await this.token.balanceOf(this.owner); + await this.tx(); + expect(await this.token.balanceOf(this.owner)).to.equal(balanceBefore); }); it('keeps same tokens by index', async function () { if (!this.token.tokenOfOwnerByIndex) return; - const tokensListed = await Promise.all([0, 1].map(i => this.token.tokenOfOwnerByIndex(owner, i))); - expect(tokensListed.map(t => t.toNumber())).to.have.members([ - firstTokenId.toNumber(), - secondTokenId.toNumber(), - ]); + + expect(await Promise.all([0n, 1n].map(i => this.token.tokenOfOwnerByIndex(this.owner, i)))).to.have.members( + [firstTokenId, secondTokenId], + ); }); }); - context('when the address of the previous owner is incorrect', function () { + describe('when the address of the previous owner is incorrect', function () { it('reverts', async function () { - await expectRevertCustomError( - transferFunction.call(this, other, other, tokenId, { from: owner }), - 'ERC721IncorrectOwner', - [other, tokenId, owner], - ); + await expect( + this.token.connect(this.owner)[fragment](this.other, this.other, tokenId, ...(opts.extra ?? [])), + ) + .to.be.revertedWithCustomError(this.token, 'ERC721IncorrectOwner') + .withArgs(this.other.address, tokenId, this.owner.address); }); }); - context('when the sender is not authorized for the token id', function () { + describe('when the sender is not authorized for the token id', function () { if (opts.unrestricted) { it('does not revert', async function () { - await transferFunction.call(this, owner, other, tokenId, { from: other }); + await this.token.connect(this.other)[fragment](this.owner, this.other, tokenId, ...(opts.extra ?? [])); }); } else { it('reverts', async function () { - await expectRevertCustomError( - transferFunction.call(this, owner, other, tokenId, { from: other }), - 'ERC721InsufficientApproval', - [other, tokenId], - ); + await expect( + this.token.connect(this.other)[fragment](this.owner, this.other, tokenId, ...(opts.extra ?? [])), + ) + .to.be.revertedWithCustomError(this.token, 'ERC721InsufficientApproval') + .withArgs(this.other.address, tokenId); }); } }); - context('when the given token ID does not exist', function () { + describe('when the given token ID does not exist', function () { it('reverts', async function () { - await expectRevertCustomError( - transferFunction.call(this, owner, other, nonExistentTokenId, { from: owner }), - 'ERC721NonexistentToken', - [nonExistentTokenId], - ); + await expect( + this.token + .connect(this.owner) + [fragment](this.owner, this.other, nonExistentTokenId, ...(opts.extra ?? [])), + ) + .to.be.revertedWithCustomError(this.token, 'ERC721NonexistentToken') + .withArgs(nonExistentTokenId); }); }); - context('when the address to transfer the token to is the zero address', function () { + describe('when the address to transfer the token to is the zero address', function () { it('reverts', async function () { - await expectRevertCustomError( - transferFunction.call(this, owner, ZERO_ADDRESS, tokenId, { from: owner }), - 'ERC721InvalidReceiver', - [ZERO_ADDRESS], - ); + await expect( + this.token.connect(this.owner)[fragment](this.owner, ethers.ZeroAddress, tokenId, ...(opts.extra ?? [])), + ) + .to.be.revertedWithCustomError(this.token, 'ERC721InvalidReceiver') + .withArgs(ethers.ZeroAddress); }); }); }; - const shouldTransferSafely = function (transferFun, data, opts = {}) { + const shouldTransferSafely = function (fragment, data, opts = {}) { + // sanity + it('function exists', async function () { + expect(this.token.interface.hasFunction(fragment)).to.be.true; + }); + describe('to a user account', function () { - shouldTransferTokensByUsers(transferFun, opts); + shouldTransferTokensByUsers(fragment, opts); }); describe('to a valid receiver contract', function () { beforeEach(async function () { - this.receiver = await ERC721ReceiverMock.new(RECEIVER_MAGIC_VALUE, RevertType.None); - this.toWhom = this.receiver.address; + this.to = await ethers.deployContract('ERC721ReceiverMock', [RECEIVER_MAGIC_VALUE, RevertType.None]); }); - shouldTransferTokensByUsers(transferFun, opts); + shouldTransferTokensByUsers(fragment, opts); it('calls onERC721Received', async function () { - const receipt = await transferFun.call(this, owner, this.receiver.address, tokenId, { from: owner }); - - await expectEvent.inTransaction(receipt.tx, ERC721ReceiverMock, 'Received', { - operator: owner, - from: owner, - tokenId: tokenId, - data: data, - }); + await expect(this.token.connect(this.owner)[fragment](this.owner, this.to, tokenId, ...(opts.extra ?? []))) + .to.emit(this.to, 'Received') + .withArgs(this.owner.address, this.owner.address, tokenId, data, anyValue); }); it('calls onERC721Received from approved', async function () { - const receipt = await transferFun.call(this, owner, this.receiver.address, tokenId, { from: approved }); - - await expectEvent.inTransaction(receipt.tx, ERC721ReceiverMock, 'Received', { - operator: approved, - from: owner, - tokenId: tokenId, - data: data, - }); + await expect( + this.token.connect(this.approved)[fragment](this.owner, this.to, tokenId, ...(opts.extra ?? [])), + ) + .to.emit(this.to, 'Received') + .withArgs(this.approved.address, this.owner.address, tokenId, data, anyValue); }); describe('with an invalid token id', function () { it('reverts', async function () { - await expectRevertCustomError( - transferFun.call(this, owner, this.receiver.address, nonExistentTokenId, { from: owner }), - 'ERC721NonexistentToken', - [nonExistentTokenId], - ); + await expect( + this.token + .connect(this.approved) + [fragment](this.owner, this.to, nonExistentTokenId, ...(opts.extra ?? [])), + ) + .to.be.revertedWithCustomError(this.token, 'ERC721NonexistentToken') + .withArgs(nonExistentTokenId); }); }); }); @@ -269,9 +287,7 @@ function shouldBehaveLikeERC721(owner, newOwner, approved, anotherApproved, oper { fnName: '$_transfer', opts: { unrestricted: true } }, ]) { describe(`via ${fnName}`, function () { - shouldTransferTokensByUsers(function (from, to, tokenId, opts) { - return this.token[fnName](from, to, tokenId, opts); - }, opts); + shouldTransferTokensByUsers(fnName, opts); }); } @@ -280,103 +296,86 @@ function shouldBehaveLikeERC721(owner, newOwner, approved, anotherApproved, oper { fnName: '$_safeTransfer', opts: { unrestricted: true } }, ]) { describe(`via ${fnName}`, function () { - const safeTransferFromWithData = function (from, to, tokenId, opts) { - return this.token.methods[fnName + '(address,address,uint256,bytes)'](from, to, tokenId, data, opts); - }; - - const safeTransferFromWithoutData = function (from, to, tokenId, opts) { - return this.token.methods[fnName + '(address,address,uint256)'](from, to, tokenId, opts); - }; - describe('with data', function () { - shouldTransferSafely(safeTransferFromWithData, data, opts); + shouldTransferSafely(fnName, data, { ...opts, extra: [ethers.Typed.bytes(data)] }); }); describe('without data', function () { - shouldTransferSafely(safeTransferFromWithoutData, null, opts); + shouldTransferSafely(fnName, '0x', opts); }); describe('to a receiver contract returning unexpected value', function () { it('reverts', async function () { - const invalidReceiver = await ERC721ReceiverMock.new('0x42', RevertType.None); - await expectRevertCustomError( - this.token.methods[fnName + '(address,address,uint256)'](owner, invalidReceiver.address, tokenId, { - from: owner, - }), - 'ERC721InvalidReceiver', - [invalidReceiver.address], - ); + const invalidReceiver = await ethers.deployContract('ERC721ReceiverMock', [ + '0xdeadbeef', + RevertType.None, + ]); + + await expect(this.token.connect(this.owner)[fnName](this.owner, invalidReceiver, tokenId)) + .to.be.revertedWithCustomError(this.token, 'ERC721InvalidReceiver') + .withArgs(invalidReceiver.target); }); }); describe('to a receiver contract that reverts with message', function () { it('reverts', async function () { - const revertingReceiver = await ERC721ReceiverMock.new( + const revertingReceiver = await ethers.deployContract('ERC721ReceiverMock', [ RECEIVER_MAGIC_VALUE, RevertType.RevertWithMessage, - ); - await expectRevert( - this.token.methods[fnName + '(address,address,uint256)'](owner, revertingReceiver.address, tokenId, { - from: owner, - }), - 'ERC721ReceiverMock: reverting', - ); + ]); + + await expect( + this.token.connect(this.owner)[fnName](this.owner, revertingReceiver, tokenId), + ).to.be.revertedWith('ERC721ReceiverMock: reverting'); }); }); describe('to a receiver contract that reverts without message', function () { it('reverts', async function () { - const revertingReceiver = await ERC721ReceiverMock.new( + const revertingReceiver = await ethers.deployContract('ERC721ReceiverMock', [ RECEIVER_MAGIC_VALUE, RevertType.RevertWithoutMessage, - ); - await expectRevertCustomError( - this.token.methods[fnName + '(address,address,uint256)'](owner, revertingReceiver.address, tokenId, { - from: owner, - }), - 'ERC721InvalidReceiver', - [revertingReceiver.address], - ); + ]); + + await expect(this.token.connect(this.owner)[fnName](this.owner, revertingReceiver, tokenId)) + .to.be.revertedWithCustomError(this.token, 'ERC721InvalidReceiver') + .withArgs(revertingReceiver.target); }); }); describe('to a receiver contract that reverts with custom error', function () { it('reverts', async function () { - const revertingReceiver = await ERC721ReceiverMock.new( + const revertingReceiver = await ethers.deployContract('ERC721ReceiverMock', [ RECEIVER_MAGIC_VALUE, RevertType.RevertWithCustomError, - ); - await expectRevertCustomError( - this.token.methods[fnName + '(address,address,uint256)'](owner, revertingReceiver.address, tokenId, { - from: owner, - }), - 'CustomError', - [RECEIVER_MAGIC_VALUE], - ); + ]); + + await expect(this.token.connect(this.owner)[fnName](this.owner, revertingReceiver, tokenId)) + .to.be.revertedWithCustomError(revertingReceiver, 'CustomError') + .withArgs(RECEIVER_MAGIC_VALUE); }); }); describe('to a receiver contract that panics', function () { it('reverts', async function () { - const revertingReceiver = await ERC721ReceiverMock.new(RECEIVER_MAGIC_VALUE, RevertType.Panic); - await expectRevert.unspecified( - this.token.methods[fnName + '(address,address,uint256)'](owner, revertingReceiver.address, tokenId, { - from: owner, - }), - ); + const revertingReceiver = await ethers.deployContract('ERC721ReceiverMock', [ + RECEIVER_MAGIC_VALUE, + RevertType.Panic, + ]); + + await expect( + this.token.connect(this.owner)[fnName](this.owner, revertingReceiver, tokenId), + ).to.be.revertedWithPanic(PANIC_CODES.DIVISION_BY_ZERO); }); }); describe('to a contract that does not implement the required function', function () { it('reverts', async function () { - const nonReceiver = await NonERC721ReceiverMock.new(); - await expectRevertCustomError( - this.token.methods[fnName + '(address,address,uint256)'](owner, nonReceiver.address, tokenId, { - from: owner, - }), - 'ERC721InvalidReceiver', - [nonReceiver.address], - ); + const nonReceiver = await ethers.deployContract('CallReceiverMock'); + + await expect(this.token.connect(this.owner)[fnName](this.owner, nonReceiver, tokenId)) + .to.be.revertedWithCustomError(this.token, 'ERC721InvalidReceiver') + .withArgs(nonReceiver.target); }); }); }); @@ -390,88 +389,90 @@ function shouldBehaveLikeERC721(owner, newOwner, approved, anotherApproved, oper describe('via safeMint', function () { // regular minting is tested in ERC721Mintable.test.js and others it('calls onERC721Received — with data', async function () { - this.receiver = await ERC721ReceiverMock.new(RECEIVER_MAGIC_VALUE, RevertType.None); - const receipt = await this.token.$_safeMint(this.receiver.address, tokenId, data); + const receiver = await ethers.deployContract('ERC721ReceiverMock', [RECEIVER_MAGIC_VALUE, RevertType.None]); - await expectEvent.inTransaction(receipt.tx, ERC721ReceiverMock, 'Received', { - from: ZERO_ADDRESS, - tokenId: tokenId, - data: data, - }); + await expect(await this.token.$_safeMint(receiver, tokenId, ethers.Typed.bytes(data))) + .to.emit(receiver, 'Received') + .withArgs(anyValue, ethers.ZeroAddress, tokenId, data, anyValue); }); it('calls onERC721Received — without data', async function () { - this.receiver = await ERC721ReceiverMock.new(RECEIVER_MAGIC_VALUE, RevertType.None); - const receipt = await this.token.$_safeMint(this.receiver.address, tokenId); + const receiver = await ethers.deployContract('ERC721ReceiverMock', [RECEIVER_MAGIC_VALUE, RevertType.None]); - await expectEvent.inTransaction(receipt.tx, ERC721ReceiverMock, 'Received', { - from: ZERO_ADDRESS, - tokenId: tokenId, - }); + await expect(await this.token.$_safeMint(receiver, tokenId)) + .to.emit(receiver, 'Received') + .withArgs(anyValue, ethers.ZeroAddress, tokenId, '0x', anyValue); }); - context('to a receiver contract returning unexpected value', function () { + describe('to a receiver contract returning unexpected value', function () { it('reverts', async function () { - const invalidReceiver = await ERC721ReceiverMock.new('0x42', RevertType.None); - await expectRevertCustomError( - this.token.$_safeMint(invalidReceiver.address, tokenId), - 'ERC721InvalidReceiver', - [invalidReceiver.address], - ); + const invalidReceiver = await ethers.deployContract('ERC721ReceiverMock', ['0xdeadbeef', RevertType.None]); + + await expect(this.token.$_safeMint(invalidReceiver, tokenId)) + .to.be.revertedWithCustomError(this.token, 'ERC721InvalidReceiver') + .withArgs(invalidReceiver.target); }); }); - context('to a receiver contract that reverts with message', function () { + describe('to a receiver contract that reverts with message', function () { it('reverts', async function () { - const revertingReceiver = await ERC721ReceiverMock.new(RECEIVER_MAGIC_VALUE, RevertType.RevertWithMessage); - await expectRevert( - this.token.$_safeMint(revertingReceiver.address, tokenId), + const revertingReceiver = await ethers.deployContract('ERC721ReceiverMock', [ + RECEIVER_MAGIC_VALUE, + RevertType.RevertWithMessage, + ]); + + await expect(this.token.$_safeMint(revertingReceiver, tokenId)).to.be.revertedWith( 'ERC721ReceiverMock: reverting', ); }); }); - context('to a receiver contract that reverts without message', function () { + describe('to a receiver contract that reverts without message', function () { it('reverts', async function () { - const revertingReceiver = await ERC721ReceiverMock.new( + const revertingReceiver = await ethers.deployContract('ERC721ReceiverMock', [ RECEIVER_MAGIC_VALUE, RevertType.RevertWithoutMessage, - ); - await expectRevertCustomError( - this.token.$_safeMint(revertingReceiver.address, tokenId), - 'ERC721InvalidReceiver', - [revertingReceiver.address], - ); + ]); + + await expect(this.token.$_safeMint(revertingReceiver, tokenId)) + .to.be.revertedWithCustomError(this.token, 'ERC721InvalidReceiver') + .withArgs(revertingReceiver.target); }); }); - context('to a receiver contract that reverts with custom error', function () { + describe('to a receiver contract that reverts with custom error', function () { it('reverts', async function () { - const revertingReceiver = await ERC721ReceiverMock.new( + const revertingReceiver = await ethers.deployContract('ERC721ReceiverMock', [ RECEIVER_MAGIC_VALUE, RevertType.RevertWithCustomError, - ); - await expectRevertCustomError(this.token.$_safeMint(revertingReceiver.address, tokenId), 'CustomError', [ - RECEIVER_MAGIC_VALUE, ]); + + await expect(this.token.$_safeMint(revertingReceiver, tokenId)) + .to.be.revertedWithCustomError(revertingReceiver, 'CustomError') + .withArgs(RECEIVER_MAGIC_VALUE); }); }); - context('to a receiver contract that panics', function () { + describe('to a receiver contract that panics', function () { it('reverts', async function () { - const revertingReceiver = await ERC721ReceiverMock.new(RECEIVER_MAGIC_VALUE, RevertType.Panic); - await expectRevert.unspecified(this.token.$_safeMint(revertingReceiver.address, tokenId)); + const revertingReceiver = await ethers.deployContract('ERC721ReceiverMock', [ + RECEIVER_MAGIC_VALUE, + RevertType.Panic, + ]); + + await expect(this.token.$_safeMint(revertingReceiver, tokenId)).to.be.revertedWithPanic( + PANIC_CODES.DIVISION_BY_ZERO, + ); }); }); - context('to a contract that does not implement the required function', function () { + describe('to a contract that does not implement the required function', function () { it('reverts', async function () { - const nonReceiver = await NonERC721ReceiverMock.new(); - await expectRevertCustomError( - this.token.$_safeMint(nonReceiver.address, tokenId), - 'ERC721InvalidReceiver', - [nonReceiver.address], - ); + const nonReceiver = await ethers.deployContract('CallReceiverMock'); + + await expect(this.token.$_safeMint(nonReceiver, tokenId)) + .to.be.revertedWithCustomError(this.token, 'ERC721InvalidReceiver') + .withArgs(nonReceiver.target); }); }); }); @@ -480,227 +481,207 @@ function shouldBehaveLikeERC721(owner, newOwner, approved, anotherApproved, oper describe('approve', function () { const tokenId = firstTokenId; - let receipt = null; - const itClearsApproval = function () { it('clears approval for the token', async function () { - expect(await this.token.getApproved(tokenId)).to.be.equal(ZERO_ADDRESS); + expect(await this.token.getApproved(tokenId)).to.equal(ethers.ZeroAddress); }); }; - const itApproves = function (address) { + const itApproves = function () { it('sets the approval for the target address', async function () { - expect(await this.token.getApproved(tokenId)).to.be.equal(address); + expect(await this.token.getApproved(tokenId)).to.equal(this.approved.address ?? this.approved); }); }; - const itEmitsApprovalEvent = function (address) { + const itEmitsApprovalEvent = function () { it('emits an approval event', async function () { - expectEvent(receipt, 'Approval', { - owner: owner, - approved: address, - tokenId: tokenId, - }); + await expect(this.tx) + .to.emit(this.token, 'Approval') + .withArgs(this.owner.address, this.approved.address ?? this.approved, tokenId); }); }; - context('when clearing approval', function () { - context('when there was no prior approval', function () { + describe('when clearing approval', function () { + describe('when there was no prior approval', function () { beforeEach(async function () { - receipt = await this.token.approve(ZERO_ADDRESS, tokenId, { from: owner }); + this.approved = ethers.ZeroAddress; + this.tx = await this.token.connect(this.owner).approve(this.approved, tokenId); }); itClearsApproval(); - itEmitsApprovalEvent(ZERO_ADDRESS); + itEmitsApprovalEvent(); }); - context('when there was a prior approval', function () { + describe('when there was a prior approval', function () { beforeEach(async function () { - await this.token.approve(approved, tokenId, { from: owner }); - receipt = await this.token.approve(ZERO_ADDRESS, tokenId, { from: owner }); + await this.token.connect(this.owner).approve(this.other, tokenId); + this.approved = ethers.ZeroAddress; + this.tx = await this.token.connect(this.owner).approve(this.approved, tokenId); }); itClearsApproval(); - itEmitsApprovalEvent(ZERO_ADDRESS); + itEmitsApprovalEvent(); }); }); - context('when approving a non-zero address', function () { - context('when there was no prior approval', function () { + describe('when approving a non-zero address', function () { + describe('when there was no prior approval', function () { beforeEach(async function () { - receipt = await this.token.approve(approved, tokenId, { from: owner }); + this.tx = await this.token.connect(this.owner).approve(this.approved, tokenId); }); - itApproves(approved); - itEmitsApprovalEvent(approved); + itApproves(); + itEmitsApprovalEvent(); }); - context('when there was a prior approval to the same address', function () { + describe('when there was a prior approval to the same address', function () { beforeEach(async function () { - await this.token.approve(approved, tokenId, { from: owner }); - receipt = await this.token.approve(approved, tokenId, { from: owner }); + await this.token.connect(this.owner).approve(this.approved, tokenId); + this.tx = await this.token.connect(this.owner).approve(this.approved, tokenId); }); - itApproves(approved); - itEmitsApprovalEvent(approved); + itApproves(); + itEmitsApprovalEvent(); }); - context('when there was a prior approval to a different address', function () { + describe('when there was a prior approval to a different address', function () { beforeEach(async function () { - await this.token.approve(anotherApproved, tokenId, { from: owner }); - receipt = await this.token.approve(anotherApproved, tokenId, { from: owner }); + await this.token.connect(this.owner).approve(this.other, tokenId); + this.tx = await this.token.connect(this.owner).approve(this.approved, tokenId); }); - itApproves(anotherApproved); - itEmitsApprovalEvent(anotherApproved); + itApproves(); + itEmitsApprovalEvent(); }); }); - context('when the sender does not own the given token ID', function () { + describe('when the sender does not own the given token ID', function () { it('reverts', async function () { - await expectRevertCustomError( - this.token.approve(approved, tokenId, { from: other }), - 'ERC721InvalidApprover', - [other], - ); + await expect(this.token.connect(this.other).approve(this.approved, tokenId)) + .to.be.revertedWithCustomError(this.token, 'ERC721InvalidApprover') + .withArgs(this.other.address); }); }); - context('when the sender is approved for the given token ID', function () { + describe('when the sender is approved for the given token ID', function () { it('reverts', async function () { - await this.token.approve(approved, tokenId, { from: owner }); - await expectRevertCustomError( - this.token.approve(anotherApproved, tokenId, { from: approved }), - 'ERC721InvalidApprover', - [approved], - ); + await this.token.connect(this.owner).approve(this.approved, tokenId); + + await expect(this.token.connect(this.approved).approve(this.other, tokenId)) + .to.be.revertedWithCustomError(this.token, 'ERC721InvalidApprover') + .withArgs(this.approved.address); }); }); - context('when the sender is an operator', function () { + describe('when the sender is an operator', function () { beforeEach(async function () { - await this.token.setApprovalForAll(operator, true, { from: owner }); - receipt = await this.token.approve(approved, tokenId, { from: operator }); + await this.token.connect(this.owner).setApprovalForAll(this.operator, true); + + this.tx = await this.token.connect(this.operator).approve(this.approved, tokenId); }); - itApproves(approved); - itEmitsApprovalEvent(approved); + itApproves(); + itEmitsApprovalEvent(); }); - context('when the given token ID does not exist', function () { + describe('when the given token ID does not exist', function () { it('reverts', async function () { - await expectRevertCustomError( - this.token.approve(approved, nonExistentTokenId, { from: operator }), - 'ERC721NonexistentToken', - [nonExistentTokenId], - ); + await expect(this.token.connect(this.operator).approve(this.approved, nonExistentTokenId)) + .to.be.revertedWithCustomError(this.token, 'ERC721NonexistentToken') + .withArgs(nonExistentTokenId); }); }); }); describe('setApprovalForAll', function () { - context('when the operator willing to approve is not the owner', function () { - context('when there is no operator approval set by the sender', function () { + describe('when the operator willing to approve is not the owner', function () { + describe('when there is no operator approval set by the sender', function () { it('approves the operator', async function () { - await this.token.setApprovalForAll(operator, true, { from: owner }); + await this.token.connect(this.owner).setApprovalForAll(this.operator, true); - expect(await this.token.isApprovedForAll(owner, operator)).to.equal(true); + expect(await this.token.isApprovedForAll(this.owner, this.operator)).to.be.true; }); it('emits an approval event', async function () { - const receipt = await this.token.setApprovalForAll(operator, true, { from: owner }); - - expectEvent(receipt, 'ApprovalForAll', { - owner: owner, - operator: operator, - approved: true, - }); + await expect(this.token.connect(this.owner).setApprovalForAll(this.operator, true)) + .to.emit(this.token, 'ApprovalForAll') + .withArgs(this.owner.address, this.operator.address, true); }); }); - context('when the operator was set as not approved', function () { + describe('when the operator was set as not approved', function () { beforeEach(async function () { - await this.token.setApprovalForAll(operator, false, { from: owner }); + await this.token.connect(this.owner).setApprovalForAll(this.operator, false); }); it('approves the operator', async function () { - await this.token.setApprovalForAll(operator, true, { from: owner }); + await this.token.connect(this.owner).setApprovalForAll(this.operator, true); - expect(await this.token.isApprovedForAll(owner, operator)).to.equal(true); + expect(await this.token.isApprovedForAll(this.owner, this.operator)).to.be.true; }); it('emits an approval event', async function () { - const receipt = await this.token.setApprovalForAll(operator, true, { from: owner }); - - expectEvent(receipt, 'ApprovalForAll', { - owner: owner, - operator: operator, - approved: true, - }); + await expect(this.token.connect(this.owner).setApprovalForAll(this.operator, true)) + .to.emit(this.token, 'ApprovalForAll') + .withArgs(this.owner.address, this.operator.address, true); }); it('can unset the operator approval', async function () { - await this.token.setApprovalForAll(operator, false, { from: owner }); + await this.token.connect(this.owner).setApprovalForAll(this.operator, false); - expect(await this.token.isApprovedForAll(owner, operator)).to.equal(false); + expect(await this.token.isApprovedForAll(this.owner, this.operator)).to.be.false; }); }); - context('when the operator was already approved', function () { + describe('when the operator was already approved', function () { beforeEach(async function () { - await this.token.setApprovalForAll(operator, true, { from: owner }); + await this.token.connect(this.owner).setApprovalForAll(this.operator, true); }); it('keeps the approval to the given address', async function () { - await this.token.setApprovalForAll(operator, true, { from: owner }); + await this.token.connect(this.owner).setApprovalForAll(this.operator, true); - expect(await this.token.isApprovedForAll(owner, operator)).to.equal(true); + expect(await this.token.isApprovedForAll(this.owner, this.operator)).to.be.true; }); it('emits an approval event', async function () { - const receipt = await this.token.setApprovalForAll(operator, true, { from: owner }); - - expectEvent(receipt, 'ApprovalForAll', { - owner: owner, - operator: operator, - approved: true, - }); + await expect(this.token.connect(this.owner).setApprovalForAll(this.operator, true)) + .to.emit(this.token, 'ApprovalForAll') + .withArgs(this.owner.address, this.operator.address, true); }); }); }); - context('when the operator is address zero', function () { + describe('when the operator is address zero', function () { it('reverts', async function () { - await expectRevertCustomError( - this.token.setApprovalForAll(constants.ZERO_ADDRESS, true, { from: owner }), - 'ERC721InvalidOperator', - [constants.ZERO_ADDRESS], - ); + await expect(this.token.connect(this.owner).setApprovalForAll(ethers.ZeroAddress, true)) + .to.be.revertedWithCustomError(this.token, 'ERC721InvalidOperator') + .withArgs(ethers.ZeroAddress); }); }); }); describe('getApproved', async function () { - context('when token is not minted', async function () { + describe('when token is not minted', async function () { it('reverts', async function () { - await expectRevertCustomError(this.token.getApproved(nonExistentTokenId), 'ERC721NonexistentToken', [ - nonExistentTokenId, - ]); + await expect(this.token.getApproved(nonExistentTokenId)) + .to.be.revertedWithCustomError(this.token, 'ERC721NonexistentToken') + .withArgs(nonExistentTokenId); }); }); - context('when token has been minted ', async function () { + describe('when token has been minted ', async function () { it('should return the zero address', async function () { - expect(await this.token.getApproved(firstTokenId)).to.be.equal(ZERO_ADDRESS); + expect(await this.token.getApproved(firstTokenId)).to.equal(ethers.ZeroAddress); }); - context('when account has been approved', async function () { + describe('when account has been approved', async function () { beforeEach(async function () { - await this.token.approve(approved, firstTokenId, { from: owner }); + await this.token.connect(this.owner).approve(this.approved, firstTokenId); }); it('returns approved account', async function () { - expect(await this.token.getApproved(firstTokenId)).to.be.equal(approved); + expect(await this.token.getApproved(firstTokenId)).to.equal(this.approved.address); }); }); }); @@ -709,243 +690,268 @@ function shouldBehaveLikeERC721(owner, newOwner, approved, anotherApproved, oper describe('_mint(address, uint256)', function () { it('reverts with a null destination address', async function () { - await expectRevertCustomError(this.token.$_mint(ZERO_ADDRESS, firstTokenId), 'ERC721InvalidReceiver', [ - ZERO_ADDRESS, - ]); + await expect(this.token.$_mint(ethers.ZeroAddress, firstTokenId)) + .to.be.revertedWithCustomError(this.token, 'ERC721InvalidReceiver') + .withArgs(ethers.ZeroAddress); }); - context('with minted token', async function () { + describe('with minted token', async function () { beforeEach(async function () { - this.receipt = await this.token.$_mint(owner, firstTokenId); + this.tx = await this.token.$_mint(this.owner, firstTokenId); }); - it('emits a Transfer event', function () { - expectEvent(this.receipt, 'Transfer', { from: ZERO_ADDRESS, to: owner, tokenId: firstTokenId }); + it('emits a Transfer event', async function () { + await expect(this.tx) + .to.emit(this.token, 'Transfer') + .withArgs(ethers.ZeroAddress, this.owner.address, firstTokenId); }); it('creates the token', async function () { - expect(await this.token.balanceOf(owner)).to.be.bignumber.equal('1'); - expect(await this.token.ownerOf(firstTokenId)).to.equal(owner); + expect(await this.token.balanceOf(this.owner)).to.equal(1n); + expect(await this.token.ownerOf(firstTokenId)).to.equal(this.owner.address); }); it('reverts when adding a token id that already exists', async function () { - await expectRevertCustomError(this.token.$_mint(owner, firstTokenId), 'ERC721InvalidSender', [ZERO_ADDRESS]); + await expect(this.token.$_mint(this.owner, firstTokenId)) + .to.be.revertedWithCustomError(this.token, 'ERC721InvalidSender') + .withArgs(ethers.ZeroAddress); }); }); }); describe('_burn', function () { it('reverts when burning a non-existent token id', async function () { - await expectRevertCustomError(this.token.$_burn(nonExistentTokenId), 'ERC721NonexistentToken', [ - nonExistentTokenId, - ]); + await expect(this.token.$_burn(nonExistentTokenId)) + .to.be.revertedWithCustomError(this.token, 'ERC721NonexistentToken') + .withArgs(nonExistentTokenId); }); - context('with minted tokens', function () { + describe('with minted tokens', function () { beforeEach(async function () { - await this.token.$_mint(owner, firstTokenId); - await this.token.$_mint(owner, secondTokenId); + await this.token.$_mint(this.owner, firstTokenId); + await this.token.$_mint(this.owner, secondTokenId); }); - context('with burnt token', function () { + describe('with burnt token', function () { beforeEach(async function () { - this.receipt = await this.token.$_burn(firstTokenId); + this.tx = await this.token.$_burn(firstTokenId); }); - it('emits a Transfer event', function () { - expectEvent(this.receipt, 'Transfer', { from: owner, to: ZERO_ADDRESS, tokenId: firstTokenId }); + it('emits a Transfer event', async function () { + await expect(this.tx) + .to.emit(this.token, 'Transfer') + .withArgs(this.owner.address, ethers.ZeroAddress, firstTokenId); }); it('deletes the token', async function () { - expect(await this.token.balanceOf(owner)).to.be.bignumber.equal('1'); - await expectRevertCustomError(this.token.ownerOf(firstTokenId), 'ERC721NonexistentToken', [firstTokenId]); + expect(await this.token.balanceOf(this.owner)).to.equal(1n); + await expect(this.token.ownerOf(firstTokenId)) + .to.be.revertedWithCustomError(this.token, 'ERC721NonexistentToken') + .withArgs(firstTokenId); }); it('reverts when burning a token id that has been deleted', async function () { - await expectRevertCustomError(this.token.$_burn(firstTokenId), 'ERC721NonexistentToken', [firstTokenId]); + await expect(this.token.$_burn(firstTokenId)) + .to.be.revertedWithCustomError(this.token, 'ERC721NonexistentToken') + .withArgs(firstTokenId); }); }); }); }); } -function shouldBehaveLikeERC721Enumerable(owner, newOwner, approved, anotherApproved, operator, other) { +function shouldBehaveLikeERC721Enumerable() { + beforeEach(async function () { + const [owner, newOwner, approved, operator, other] = this.accounts; + Object.assign(this, { owner, newOwner, approved, operator, other }); + }); + shouldSupportInterfaces(['ERC721Enumerable']); - context('with minted tokens', function () { + describe('with minted tokens', function () { beforeEach(async function () { - await this.token.$_mint(owner, firstTokenId); - await this.token.$_mint(owner, secondTokenId); - this.toWhom = other; // default to other for toWhom in context-dependent tests + await this.token.$_mint(this.owner, firstTokenId); + await this.token.$_mint(this.owner, secondTokenId); + this.to = this.other; }); describe('totalSupply', function () { it('returns total token supply', async function () { - expect(await this.token.totalSupply()).to.be.bignumber.equal('2'); + expect(await this.token.totalSupply()).to.equal(2n); }); }); describe('tokenOfOwnerByIndex', function () { describe('when the given index is lower than the amount of tokens owned by the given address', function () { it('returns the token ID placed at the given index', async function () { - expect(await this.token.tokenOfOwnerByIndex(owner, 0)).to.be.bignumber.equal(firstTokenId); + expect(await this.token.tokenOfOwnerByIndex(this.owner, 0n)).to.equal(firstTokenId); }); }); describe('when the index is greater than or equal to the total tokens owned by the given address', function () { it('reverts', async function () { - await expectRevertCustomError(this.token.tokenOfOwnerByIndex(owner, 2), 'ERC721OutOfBoundsIndex', [owner, 2]); + await expect(this.token.tokenOfOwnerByIndex(this.owner, 2n)) + .to.be.revertedWithCustomError(this.token, 'ERC721OutOfBoundsIndex') + .withArgs(this.owner.address, 2n); }); }); describe('when the given address does not own any token', function () { it('reverts', async function () { - await expectRevertCustomError(this.token.tokenOfOwnerByIndex(other, 0), 'ERC721OutOfBoundsIndex', [other, 0]); + await expect(this.token.tokenOfOwnerByIndex(this.other, 0n)) + .to.be.revertedWithCustomError(this.token, 'ERC721OutOfBoundsIndex') + .withArgs(this.other.address, 0n); }); }); describe('after transferring all tokens to another user', function () { beforeEach(async function () { - await this.token.transferFrom(owner, other, firstTokenId, { from: owner }); - await this.token.transferFrom(owner, other, secondTokenId, { from: owner }); + await this.token.connect(this.owner).transferFrom(this.owner, this.other, firstTokenId); + await this.token.connect(this.owner).transferFrom(this.owner, this.other, secondTokenId); }); it('returns correct token IDs for target', async function () { - expect(await this.token.balanceOf(other)).to.be.bignumber.equal('2'); - const tokensListed = await Promise.all([0, 1].map(i => this.token.tokenOfOwnerByIndex(other, i))); - expect(tokensListed.map(t => t.toNumber())).to.have.members([ - firstTokenId.toNumber(), - secondTokenId.toNumber(), + expect(await this.token.balanceOf(this.other)).to.equal(2n); + + expect(await Promise.all([0n, 1n].map(i => this.token.tokenOfOwnerByIndex(this.other, i)))).to.have.members([ + firstTokenId, + secondTokenId, ]); }); it('returns empty collection for original owner', async function () { - expect(await this.token.balanceOf(owner)).to.be.bignumber.equal('0'); - await expectRevertCustomError(this.token.tokenOfOwnerByIndex(owner, 0), 'ERC721OutOfBoundsIndex', [owner, 0]); + expect(await this.token.balanceOf(this.owner)).to.equal(0n); + await expect(this.token.tokenOfOwnerByIndex(this.owner, 0n)) + .to.be.revertedWithCustomError(this.token, 'ERC721OutOfBoundsIndex') + .withArgs(this.owner.address, 0n); }); }); }); describe('tokenByIndex', function () { it('returns all tokens', async function () { - const tokensListed = await Promise.all([0, 1].map(i => this.token.tokenByIndex(i))); - expect(tokensListed.map(t => t.toNumber())).to.have.members([ - firstTokenId.toNumber(), - secondTokenId.toNumber(), + expect(await Promise.all([0n, 1n].map(i => this.token.tokenByIndex(i)))).to.have.members([ + firstTokenId, + secondTokenId, ]); }); it('reverts if index is greater than supply', async function () { - await expectRevertCustomError(this.token.tokenByIndex(2), 'ERC721OutOfBoundsIndex', [ZERO_ADDRESS, 2]); + await expect(this.token.tokenByIndex(2n)) + .to.be.revertedWithCustomError(this.token, 'ERC721OutOfBoundsIndex') + .withArgs(ethers.ZeroAddress, 2n); }); - [firstTokenId, secondTokenId].forEach(function (tokenId) { + for (const tokenId of [firstTokenId, secondTokenId]) { it(`returns all tokens after burning token ${tokenId} and minting new tokens`, async function () { - const newTokenId = new BN(300); - const anotherNewTokenId = new BN(400); + const newTokenId = 300n; + const anotherNewTokenId = 400n; await this.token.$_burn(tokenId); - await this.token.$_mint(newOwner, newTokenId); - await this.token.$_mint(newOwner, anotherNewTokenId); + await this.token.$_mint(this.newOwner, newTokenId); + await this.token.$_mint(this.newOwner, anotherNewTokenId); - expect(await this.token.totalSupply()).to.be.bignumber.equal('3'); + expect(await this.token.totalSupply()).to.equal(3n); - const tokensListed = await Promise.all([0, 1, 2].map(i => this.token.tokenByIndex(i))); - const expectedTokens = [firstTokenId, secondTokenId, newTokenId, anotherNewTokenId].filter( - x => x !== tokenId, - ); - expect(tokensListed.map(t => t.toNumber())).to.have.members(expectedTokens.map(t => t.toNumber())); + expect(await Promise.all([0n, 1n, 2n].map(i => this.token.tokenByIndex(i)))) + .to.have.members([firstTokenId, secondTokenId, newTokenId, anotherNewTokenId].filter(x => x !== tokenId)) + .to.not.include(tokenId); }); - }); + } }); }); describe('_mint(address, uint256)', function () { it('reverts with a null destination address', async function () { - await expectRevertCustomError(this.token.$_mint(ZERO_ADDRESS, firstTokenId), 'ERC721InvalidReceiver', [ - ZERO_ADDRESS, - ]); + await expect(this.token.$_mint(ethers.ZeroAddress, firstTokenId)) + .to.be.revertedWithCustomError(this.token, 'ERC721InvalidReceiver') + .withArgs(ethers.ZeroAddress); }); - context('with minted token', async function () { + describe('with minted token', async function () { beforeEach(async function () { - this.receipt = await this.token.$_mint(owner, firstTokenId); + await this.token.$_mint(this.owner, firstTokenId); }); it('adjusts owner tokens by index', async function () { - expect(await this.token.tokenOfOwnerByIndex(owner, 0)).to.be.bignumber.equal(firstTokenId); + expect(await this.token.tokenOfOwnerByIndex(this.owner, 0n)).to.equal(firstTokenId); }); it('adjusts all tokens list', async function () { - expect(await this.token.tokenByIndex(0)).to.be.bignumber.equal(firstTokenId); + expect(await this.token.tokenByIndex(0n)).to.equal(firstTokenId); }); }); }); describe('_burn', function () { it('reverts when burning a non-existent token id', async function () { - await expectRevertCustomError(this.token.$_burn(firstTokenId), 'ERC721NonexistentToken', [firstTokenId]); + await expect(this.token.$_burn(firstTokenId)) + .to.be.revertedWithCustomError(this.token, 'ERC721NonexistentToken') + .withArgs(firstTokenId); }); - context('with minted tokens', function () { + describe('with minted tokens', function () { beforeEach(async function () { - await this.token.$_mint(owner, firstTokenId); - await this.token.$_mint(owner, secondTokenId); + await this.token.$_mint(this.owner, firstTokenId); + await this.token.$_mint(this.owner, secondTokenId); }); - context('with burnt token', function () { + describe('with burnt token', function () { beforeEach(async function () { - this.receipt = await this.token.$_burn(firstTokenId); + await this.token.$_burn(firstTokenId); }); it('removes that token from the token list of the owner', async function () { - expect(await this.token.tokenOfOwnerByIndex(owner, 0)).to.be.bignumber.equal(secondTokenId); + expect(await this.token.tokenOfOwnerByIndex(this.owner, 0n)).to.equal(secondTokenId); }); it('adjusts all tokens list', async function () { - expect(await this.token.tokenByIndex(0)).to.be.bignumber.equal(secondTokenId); + expect(await this.token.tokenByIndex(0n)).to.equal(secondTokenId); }); it('burns all tokens', async function () { - await this.token.$_burn(secondTokenId, { from: owner }); - expect(await this.token.totalSupply()).to.be.bignumber.equal('0'); - await expectRevertCustomError(this.token.tokenByIndex(0), 'ERC721OutOfBoundsIndex', [ZERO_ADDRESS, 0]); + await this.token.$_burn(secondTokenId); + expect(await this.token.totalSupply()).to.equal(0n); + + await expect(this.token.tokenByIndex(0n)) + .to.be.revertedWithCustomError(this.token, 'ERC721OutOfBoundsIndex') + .withArgs(ethers.ZeroAddress, 0n); }); }); }); }); } -function shouldBehaveLikeERC721Metadata(name, symbol, owner) { +function shouldBehaveLikeERC721Metadata(name, symbol) { shouldSupportInterfaces(['ERC721Metadata']); describe('metadata', function () { it('has a name', async function () { - expect(await this.token.name()).to.be.equal(name); + expect(await this.token.name()).to.equal(name); }); it('has a symbol', async function () { - expect(await this.token.symbol()).to.be.equal(symbol); + expect(await this.token.symbol()).to.equal(symbol); }); describe('token URI', function () { beforeEach(async function () { - await this.token.$_mint(owner, firstTokenId); + await this.token.$_mint(this.owner, firstTokenId); }); it('return empty string by default', async function () { - expect(await this.token.tokenURI(firstTokenId)).to.be.equal(''); + expect(await this.token.tokenURI(firstTokenId)).to.equal(''); }); it('reverts when queried for non existent token id', async function () { - await expectRevertCustomError(this.token.tokenURI(nonExistentTokenId), 'ERC721NonexistentToken', [ - nonExistentTokenId, - ]); + await expect(this.token.tokenURI(nonExistentTokenId)) + .to.be.revertedWithCustomError(this.token, 'ERC721NonexistentToken') + .withArgs(nonExistentTokenId); }); describe('base URI', function () { beforeEach(function () { - if (this.token.setBaseURI === undefined) { + if (!this.token.interface.hasFunction('setBaseURI')) { this.skip(); } }); @@ -957,14 +963,14 @@ function shouldBehaveLikeERC721Metadata(name, symbol, owner) { it('base URI is added as a prefix to the token URI', async function () { await this.token.setBaseURI(baseURI); - expect(await this.token.tokenURI(firstTokenId)).to.be.equal(baseURI + firstTokenId.toString()); + expect(await this.token.tokenURI(firstTokenId)).to.equal(baseURI + firstTokenId.toString()); }); it('token URI can be changed by changing the base URI', async function () { await this.token.setBaseURI(baseURI); const newBaseURI = 'https://api.example.com/v2/'; await this.token.setBaseURI(newBaseURI); - expect(await this.token.tokenURI(firstTokenId)).to.be.equal(newBaseURI + firstTokenId.toString()); + expect(await this.token.tokenURI(firstTokenId)).to.equal(newBaseURI + firstTokenId.toString()); }); }); }); diff --git a/test/token/ERC721/ERC721.test.js b/test/token/ERC721/ERC721.test.js index 372dd5069d0..1454cb057c6 100644 --- a/test/token/ERC721/ERC721.test.js +++ b/test/token/ERC721/ERC721.test.js @@ -1,15 +1,23 @@ +const { ethers } = require('hardhat'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); + const { shouldBehaveLikeERC721, shouldBehaveLikeERC721Metadata } = require('./ERC721.behavior'); -const ERC721 = artifacts.require('$ERC721'); +const name = 'Non Fungible Token'; +const symbol = 'NFT'; -contract('ERC721', function (accounts) { - const name = 'Non Fungible Token'; - const symbol = 'NFT'; +async function fixture() { + return { + accounts: await ethers.getSigners(), + token: await ethers.deployContract('$ERC721', [name, symbol]), + }; +} +describe('ERC721', function () { beforeEach(async function () { - this.token = await ERC721.new(name, symbol); + Object.assign(this, await loadFixture(fixture)); }); - shouldBehaveLikeERC721(...accounts); - shouldBehaveLikeERC721Metadata(name, symbol, ...accounts); + shouldBehaveLikeERC721(); + shouldBehaveLikeERC721Metadata(name, symbol); }); diff --git a/test/token/ERC721/ERC721Enumerable.test.js b/test/token/ERC721/ERC721Enumerable.test.js index 31c28d177b5..a3bdea73f5a 100644 --- a/test/token/ERC721/ERC721Enumerable.test.js +++ b/test/token/ERC721/ERC721Enumerable.test.js @@ -1,20 +1,28 @@ +const { ethers } = require('hardhat'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); + const { shouldBehaveLikeERC721, shouldBehaveLikeERC721Metadata, shouldBehaveLikeERC721Enumerable, } = require('./ERC721.behavior'); -const ERC721Enumerable = artifacts.require('$ERC721Enumerable'); +const name = 'Non Fungible Token'; +const symbol = 'NFT'; -contract('ERC721Enumerable', function (accounts) { - const name = 'Non Fungible Token'; - const symbol = 'NFT'; +async function fixture() { + return { + accounts: await ethers.getSigners(), + token: await ethers.deployContract('$ERC721Enumerable', [name, symbol]), + }; +} +describe('ERC721', function () { beforeEach(async function () { - this.token = await ERC721Enumerable.new(name, symbol); + Object.assign(this, await loadFixture(fixture)); }); - shouldBehaveLikeERC721(...accounts); - shouldBehaveLikeERC721Metadata(name, symbol, ...accounts); - shouldBehaveLikeERC721Enumerable(...accounts); + shouldBehaveLikeERC721(); + shouldBehaveLikeERC721Metadata(name, symbol); + shouldBehaveLikeERC721Enumerable(); }); diff --git a/test/token/ERC721/extensions/ERC721Burnable.test.js b/test/token/ERC721/extensions/ERC721Burnable.test.js index df059e09078..0a5e838fec8 100644 --- a/test/token/ERC721/extensions/ERC721Burnable.test.js +++ b/test/token/ERC721/extensions/ERC721Burnable.test.js @@ -1,80 +1,75 @@ -const { BN, constants, expectEvent } = require('@openzeppelin/test-helpers'); - +const { ethers } = require('hardhat'); const { expect } = require('chai'); -const { expectRevertCustomError } = require('../../../helpers/customError'); - -const ERC721Burnable = artifacts.require('$ERC721Burnable'); - -contract('ERC721Burnable', function (accounts) { - const [owner, approved, another] = accounts; +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); - const firstTokenId = new BN(1); - const secondTokenId = new BN(2); - const unknownTokenId = new BN(3); +const name = 'Non Fungible Token'; +const symbol = 'NFT'; +const tokenId = 1n; +const otherTokenId = 2n; +const unknownTokenId = 3n; - const name = 'Non Fungible Token'; - const symbol = 'NFT'; +async function fixture() { + const [owner, approved, another] = await ethers.getSigners(); + const token = await ethers.deployContract('$ERC721Burnable', [name, symbol]); + return { owner, approved, another, token }; +} +describe('ERC721Burnable', function () { beforeEach(async function () { - this.token = await ERC721Burnable.new(name, symbol); + Object.assign(this, await loadFixture(fixture)); }); describe('like a burnable ERC721', function () { beforeEach(async function () { - await this.token.$_mint(owner, firstTokenId); - await this.token.$_mint(owner, secondTokenId); + await this.token.$_mint(this.owner, tokenId); + await this.token.$_mint(this.owner, otherTokenId); }); describe('burn', function () { - const tokenId = firstTokenId; - let receipt = null; - describe('when successful', function () { - beforeEach(async function () { - receipt = await this.token.burn(tokenId, { from: owner }); - }); + it('emits a burn event, burns the given token ID and adjusts the balance of the owner', async function () { + const balanceBefore = await this.token.balanceOf(this.owner); - it('burns the given token ID and adjusts the balance of the owner', async function () { - await expectRevertCustomError(this.token.ownerOf(tokenId), 'ERC721NonexistentToken', [tokenId]); - expect(await this.token.balanceOf(owner)).to.be.bignumber.equal('1'); - }); + await expect(this.token.connect(this.owner).burn(tokenId)) + .to.emit(this.token, 'Transfer') + .withArgs(this.owner.address, ethers.ZeroAddress, tokenId); - it('emits a burn event', async function () { - expectEvent(receipt, 'Transfer', { - from: owner, - to: constants.ZERO_ADDRESS, - tokenId: tokenId, - }); + await expect(this.token.ownerOf(tokenId)) + .to.be.revertedWithCustomError(this.token, 'ERC721NonexistentToken') + .withArgs(tokenId); + + expect(await this.token.balanceOf(this.owner)).to.equal(balanceBefore - 1n); }); }); describe('when there is a previous approval burned', function () { beforeEach(async function () { - await this.token.approve(approved, tokenId, { from: owner }); - receipt = await this.token.burn(tokenId, { from: owner }); + await this.token.connect(this.owner).approve(this.approved, tokenId); + await this.token.connect(this.owner).burn(tokenId); }); context('getApproved', function () { it('reverts', async function () { - await expectRevertCustomError(this.token.getApproved(tokenId), 'ERC721NonexistentToken', [tokenId]); + await expect(this.token.getApproved(tokenId)) + .to.be.revertedWithCustomError(this.token, 'ERC721NonexistentToken') + .withArgs(tokenId); }); }); }); describe('when there is no previous approval burned', function () { it('reverts', async function () { - await expectRevertCustomError(this.token.burn(tokenId, { from: another }), 'ERC721InsufficientApproval', [ - another, - tokenId, - ]); + await expect(this.token.connect(this.another).burn(tokenId)) + .to.be.revertedWithCustomError(this.token, 'ERC721InsufficientApproval') + .withArgs(this.another.address, tokenId); }); }); describe('when the given token ID was not tracked by this contract', function () { it('reverts', async function () { - await expectRevertCustomError(this.token.burn(unknownTokenId, { from: owner }), 'ERC721NonexistentToken', [ - unknownTokenId, - ]); + await expect(this.token.connect(this.owner).burn(unknownTokenId)) + .to.be.revertedWithCustomError(this.token, 'ERC721NonexistentToken') + .withArgs(unknownTokenId); }); }); }); diff --git a/test/token/ERC721/extensions/ERC721Consecutive.test.js b/test/token/ERC721/extensions/ERC721Consecutive.test.js index e4ee3196d44..b83e2feb4cf 100644 --- a/test/token/ERC721/extensions/ERC721Consecutive.test.js +++ b/test/token/ERC721/extensions/ERC721Consecutive.test.js @@ -1,55 +1,60 @@ -const { constants, expectEvent } = require('@openzeppelin/test-helpers'); +const { ethers } = require('hardhat'); const { expect } = require('chai'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); + const { sum } = require('../../../helpers/math'); -const { expectRevertCustomError } = require('../../../helpers/customError'); -const { ZERO_ADDRESS } = require('@openzeppelin/test-helpers/src/constants'); - -const ERC721ConsecutiveMock = artifacts.require('$ERC721ConsecutiveMock'); -const ERC721ConsecutiveEnumerableMock = artifacts.require('$ERC721ConsecutiveEnumerableMock'); -const ERC721ConsecutiveNoConstructorMintMock = artifacts.require('$ERC721ConsecutiveNoConstructorMintMock'); - -contract('ERC721Consecutive', function (accounts) { - const [user1, user2, user3, receiver] = accounts; - - const name = 'Non Fungible Token'; - const symbol = 'NFT'; - const batches = [ - { receiver: user1, amount: 0 }, - { receiver: user1, amount: 1 }, - { receiver: user1, amount: 2 }, - { receiver: user2, amount: 5 }, - { receiver: user3, amount: 0 }, - { receiver: user1, amount: 7 }, - ]; - const delegates = [user1, user3]; - - for (const offset of [0, 1, 42]) { + +const name = 'Non Fungible Token'; +const symbol = 'NFT'; + +describe('ERC721Consecutive', function () { + for (const offset of [0n, 1n, 42n]) { describe(`with offset ${offset}`, function () { - beforeEach(async function () { - this.token = await ERC721ConsecutiveMock.new( + async function fixture() { + const accounts = await ethers.getSigners(); + const [alice, bruce, chris, receiver] = accounts; + + const batches = [ + { receiver: alice, amount: 0n }, + { receiver: alice, amount: 1n }, + { receiver: alice, amount: 2n }, + { receiver: bruce, amount: 5n }, + { receiver: chris, amount: 0n }, + { receiver: alice, amount: 7n }, + ]; + const delegates = [alice, chris]; + + const token = await ethers.deployContract('$ERC721ConsecutiveMock', [ name, symbol, offset, delegates, batches.map(({ receiver }) => receiver), batches.map(({ amount }) => amount), - ); + ]); + + return { accounts, alice, bruce, chris, receiver, batches, delegates, token }; + } + + beforeEach(async function () { + Object.assign(this, await loadFixture(fixture)); }); describe('minting during construction', function () { it('events are emitted at construction', async function () { let first = offset; - - for (const batch of batches) { + for (const batch of this.batches) { if (batch.amount > 0) { - await expectEvent.inConstruction(this.token, 'ConsecutiveTransfer', { - fromTokenId: web3.utils.toBN(first), - toTokenId: web3.utils.toBN(first + batch.amount - 1), - fromAddress: constants.ZERO_ADDRESS, - toAddress: batch.receiver, - }); + await expect(this.token.deploymentTransaction()) + .to.emit(this.token, 'ConsecutiveTransfer') + .withArgs( + first /* fromTokenId */, + first + batch.amount - 1n /* toTokenId */, + ethers.ZeroAddress /* fromAddress */, + batch.receiver.address /* toAddress */, + ); } else { - // expectEvent.notEmitted.inConstruction only looks at event name, and doesn't check the parameters + // ".to.not.emit" only looks at event name, and doesn't check the parameters } first += batch.amount; } @@ -57,166 +62,175 @@ contract('ERC721Consecutive', function (accounts) { it('ownership is set', async function () { const owners = [ - ...Array(offset).fill(constants.ZERO_ADDRESS), - ...batches.flatMap(({ receiver, amount }) => Array(amount).fill(receiver)), + ...Array(Number(offset)).fill(ethers.ZeroAddress), + ...this.batches.flatMap(({ receiver, amount }) => Array(Number(amount)).fill(receiver.address)), ]; for (const tokenId in owners) { - if (owners[tokenId] != constants.ZERO_ADDRESS) { - expect(await this.token.ownerOf(tokenId)).to.be.equal(owners[tokenId]); + if (owners[tokenId] != ethers.ZeroAddress) { + expect(await this.token.ownerOf(tokenId)).to.equal(owners[tokenId]); } } }); it('balance & voting power are set', async function () { - for (const account of accounts) { + for (const account of this.accounts) { const balance = - sum(...batches.filter(({ receiver }) => receiver === account).map(({ amount }) => amount)) ?? 0; + sum(...this.batches.filter(({ receiver }) => receiver === account).map(({ amount }) => amount)) ?? 0n; - expect(await this.token.balanceOf(account)).to.be.bignumber.equal(web3.utils.toBN(balance)); + expect(await this.token.balanceOf(account)).to.equal(balance); // If not delegated at construction, check before + do delegation - if (!delegates.includes(account)) { - expect(await this.token.getVotes(account)).to.be.bignumber.equal(web3.utils.toBN(0)); + if (!this.delegates.includes(account)) { + expect(await this.token.getVotes(account)).to.equal(0n); - await this.token.delegate(account, { from: account }); + await this.token.connect(account).delegate(account); } // At this point all accounts should have delegated - expect(await this.token.getVotes(account)).to.be.bignumber.equal(web3.utils.toBN(balance)); + expect(await this.token.getVotes(account)).to.equal(balance); } }); it('reverts on consecutive minting to the zero address', async function () { - await expectRevertCustomError( - ERC721ConsecutiveMock.new(name, symbol, offset, delegates, [ZERO_ADDRESS], [10]), - 'ERC721InvalidReceiver', - [ZERO_ADDRESS], - ); + await expect( + ethers.deployContract('$ERC721ConsecutiveMock', [ + name, + symbol, + offset, + this.delegates, + [ethers.ZeroAddress], + [10], + ]), + ) + .to.be.revertedWithCustomError(this.token, 'ERC721InvalidReceiver') + .withArgs(ethers.ZeroAddress); }); }); describe('minting after construction', function () { it('consecutive minting is not possible after construction', async function () { - await expectRevertCustomError(this.token.$_mintConsecutive(user1, 10), 'ERC721ForbiddenBatchMint', []); + await expect(this.token.$_mintConsecutive(this.alice, 10)).to.be.revertedWithCustomError( + this.token, + 'ERC721ForbiddenBatchMint', + ); }); it('simple minting is possible after construction', async function () { - const tokenId = sum(...batches.map(b => b.amount)) + offset; + const tokenId = sum(...this.batches.map(b => b.amount)) + offset; - await expectRevertCustomError(this.token.ownerOf(tokenId), 'ERC721NonexistentToken', [tokenId]); + await expect(this.token.ownerOf(tokenId)) + .to.be.revertedWithCustomError(this.token, 'ERC721NonexistentToken') + .withArgs(tokenId); - expectEvent(await this.token.$_mint(user1, tokenId), 'Transfer', { - from: constants.ZERO_ADDRESS, - to: user1, - tokenId: tokenId.toString(), - }); + await expect(this.token.$_mint(this.alice, tokenId)) + .to.emit(this.token, 'Transfer') + .withArgs(ethers.ZeroAddress, this.alice.address, tokenId); }); it('cannot mint a token that has been batched minted', async function () { - const tokenId = sum(...batches.map(b => b.amount)) + offset - 1; + const tokenId = sum(...this.batches.map(b => b.amount)) + offset - 1n; - expect(await this.token.ownerOf(tokenId)).to.be.not.equal(constants.ZERO_ADDRESS); + expect(await this.token.ownerOf(tokenId)).to.not.equal(ethers.ZeroAddress); - await expectRevertCustomError(this.token.$_mint(user1, tokenId), 'ERC721InvalidSender', [ZERO_ADDRESS]); + await expect(this.token.$_mint(this.alice, tokenId)) + .to.be.revertedWithCustomError(this.token, 'ERC721InvalidSender') + .withArgs(ethers.ZeroAddress); }); }); describe('ERC721 behavior', function () { - const tokenId = web3.utils.toBN(offset + 1); + const tokenId = offset + 1n; it('core takes over ownership on transfer', async function () { - await this.token.transferFrom(user1, receiver, tokenId, { from: user1 }); + await this.token.connect(this.alice).transferFrom(this.alice, this.receiver, tokenId); - expect(await this.token.ownerOf(tokenId)).to.be.equal(receiver); + expect(await this.token.ownerOf(tokenId)).to.equal(this.receiver.address); }); it('tokens can be burned and re-minted #1', async function () { - expectEvent(await this.token.$_burn(tokenId, { from: user1 }), 'Transfer', { - from: user1, - to: constants.ZERO_ADDRESS, - tokenId, - }); + await expect(this.token.connect(this.alice).$_burn(tokenId)) + .to.emit(this.token, 'Transfer') + .withArgs(this.alice.address, ethers.ZeroAddress, tokenId); - await expectRevertCustomError(this.token.ownerOf(tokenId), 'ERC721NonexistentToken', [tokenId]); + await expect(this.token.ownerOf(tokenId)) + .to.be.revertedWithCustomError(this.token, 'ERC721NonexistentToken') + .withArgs(tokenId); - expectEvent(await this.token.$_mint(user2, tokenId), 'Transfer', { - from: constants.ZERO_ADDRESS, - to: user2, - tokenId, - }); + await expect(this.token.$_mint(this.bruce, tokenId)) + .to.emit(this.token, 'Transfer') + .withArgs(ethers.ZeroAddress, this.bruce.address, tokenId); - expect(await this.token.ownerOf(tokenId)).to.be.equal(user2); + expect(await this.token.ownerOf(tokenId)).to.equal(this.bruce.address); }); it('tokens can be burned and re-minted #2', async function () { - const tokenId = web3.utils.toBN(sum(...batches.map(({ amount }) => amount)) + offset); + const tokenId = sum(...this.batches.map(({ amount }) => amount)) + offset; - await expectRevertCustomError(this.token.ownerOf(tokenId), 'ERC721NonexistentToken', [tokenId]); + await expect(this.token.ownerOf(tokenId)) + .to.be.revertedWithCustomError(this.token, 'ERC721NonexistentToken') + .withArgs(tokenId); // mint - await this.token.$_mint(user1, tokenId); + await expect(this.token.$_mint(this.alice, tokenId)) + .to.emit(this.token, 'Transfer') + .withArgs(ethers.ZeroAddress, this.alice.address, tokenId); - expect(await this.token.ownerOf(tokenId), user1); + expect(await this.token.ownerOf(tokenId)).to.equal(this.alice.address); // burn - expectEvent(await this.token.$_burn(tokenId, { from: user1 }), 'Transfer', { - from: user1, - to: constants.ZERO_ADDRESS, - tokenId, - }); + await expect(await this.token.$_burn(tokenId)) + .to.emit(this.token, 'Transfer') + .withArgs(this.alice.address, ethers.ZeroAddress, tokenId); - await expectRevertCustomError(this.token.ownerOf(tokenId), 'ERC721NonexistentToken', [tokenId]); + await expect(this.token.ownerOf(tokenId)) + .to.be.revertedWithCustomError(this.token, 'ERC721NonexistentToken') + .withArgs(tokenId); // re-mint - expectEvent(await this.token.$_mint(user2, tokenId), 'Transfer', { - from: constants.ZERO_ADDRESS, - to: user2, - tokenId, - }); + await expect(this.token.$_mint(this.bruce, tokenId)) + .to.emit(this.token, 'Transfer') + .withArgs(ethers.ZeroAddress, this.bruce.address, tokenId); - expect(await this.token.ownerOf(tokenId), user2); + expect(await this.token.ownerOf(tokenId)).to.equal(this.bruce.address); }); }); }); } describe('invalid use', function () { + const receiver = ethers.Wallet.createRandom(); + it('cannot mint a batch larger than 5000', async function () { - await expectRevertCustomError( - ERC721ConsecutiveMock.new(name, symbol, 0, [], [user1], ['5001']), - 'ERC721ExceededMaxBatchMint', - [5000, 5001], - ); + const { interface } = await ethers.getContractFactory('$ERC721ConsecutiveMock'); + + await expect(ethers.deployContract('$ERC721ConsecutiveMock', [name, symbol, 0, [], [receiver], [5001n]])) + .to.be.revertedWithCustomError({ interface }, 'ERC721ExceededMaxBatchMint') + .withArgs(5001n, 5000n); }); it('cannot use single minting during construction', async function () { - await expectRevertCustomError( - ERC721ConsecutiveNoConstructorMintMock.new(name, symbol), - 'ERC721ForbiddenMint', - [], - ); + const { interface } = await ethers.getContractFactory('$ERC721ConsecutiveNoConstructorMintMock'); + + await expect( + ethers.deployContract('$ERC721ConsecutiveNoConstructorMintMock', [name, symbol]), + ).to.be.revertedWithCustomError({ interface }, 'ERC721ForbiddenMint'); }); it('cannot use single minting during construction', async function () { - await expectRevertCustomError( - ERC721ConsecutiveNoConstructorMintMock.new(name, symbol), - 'ERC721ForbiddenMint', - [], - ); + const { interface } = await ethers.getContractFactory('$ERC721ConsecutiveNoConstructorMintMock'); + + await expect( + ethers.deployContract('$ERC721ConsecutiveNoConstructorMintMock', [name, symbol]), + ).to.be.revertedWithCustomError({ interface }, 'ERC721ForbiddenMint'); }); it('consecutive mint not compatible with enumerability', async function () { - await expectRevertCustomError( - ERC721ConsecutiveEnumerableMock.new( - name, - symbol, - batches.map(({ receiver }) => receiver), - batches.map(({ amount }) => amount), - ), - 'ERC721EnumerableForbiddenBatchMint', - [], - ); + const { interface } = await ethers.getContractFactory('$ERC721ConsecutiveEnumerableMock'); + + await expect( + ethers.deployContract('$ERC721ConsecutiveEnumerableMock', [name, symbol, [receiver], [100n]]), + ).to.be.revertedWithCustomError({ interface }, 'ERC721EnumerableForbiddenBatchMint'); }); }); }); diff --git a/test/token/ERC721/extensions/ERC721Pausable.test.js b/test/token/ERC721/extensions/ERC721Pausable.test.js index 5d77149f2b3..2be0c902dfe 100644 --- a/test/token/ERC721/extensions/ERC721Pausable.test.js +++ b/test/token/ERC721/extensions/ERC721Pausable.test.js @@ -1,89 +1,80 @@ -const { BN, constants } = require('@openzeppelin/test-helpers'); - +const { ethers } = require('hardhat'); const { expect } = require('chai'); -const { expectRevertCustomError } = require('../../../helpers/customError'); - -const ERC721Pausable = artifacts.require('$ERC721Pausable'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); -contract('ERC721Pausable', function (accounts) { - const [owner, receiver, operator] = accounts; +const name = 'Non Fungible Token'; +const symbol = 'NFT'; +const tokenId = 1n; +const otherTokenId = 2n; +const data = ethers.Typed.bytes('0x42'); - const name = 'Non Fungible Token'; - const symbol = 'NFT'; +async function fixture() { + const [owner, receiver, operator] = await ethers.getSigners(); + const token = await ethers.deployContract('$ERC721Pausable', [name, symbol]); + return { owner, receiver, operator, token }; +} +describe('ERC721Pausable', function () { beforeEach(async function () { - this.token = await ERC721Pausable.new(name, symbol); + Object.assign(this, await loadFixture(fixture)); }); context('when token is paused', function () { - const firstTokenId = new BN(1); - const secondTokenId = new BN(1337); - - const mockData = '0x42'; - beforeEach(async function () { - await this.token.$_mint(owner, firstTokenId, { from: owner }); + await this.token.$_mint(this.owner, tokenId); await this.token.$_pause(); }); it('reverts when trying to transferFrom', async function () { - await expectRevertCustomError( - this.token.transferFrom(owner, receiver, firstTokenId, { from: owner }), - 'EnforcedPause', - [], - ); + await expect( + this.token.connect(this.owner).transferFrom(this.owner, this.receiver, tokenId), + ).to.be.revertedWithCustomError(this.token, 'EnforcedPause'); }); it('reverts when trying to safeTransferFrom', async function () { - await expectRevertCustomError( - this.token.safeTransferFrom(owner, receiver, firstTokenId, { from: owner }), - 'EnforcedPause', - [], - ); + await expect( + this.token.connect(this.owner).safeTransferFrom(this.owner, this.receiver, tokenId), + ).to.be.revertedWithCustomError(this.token, 'EnforcedPause'); }); it('reverts when trying to safeTransferFrom with data', async function () { - await expectRevertCustomError( - this.token.methods['safeTransferFrom(address,address,uint256,bytes)'](owner, receiver, firstTokenId, mockData, { - from: owner, - }), - 'EnforcedPause', - [], - ); + await expect( + this.token.connect(this.owner).safeTransferFrom(this.owner, this.receiver, tokenId, data), + ).to.be.revertedWithCustomError(this.token, 'EnforcedPause'); }); it('reverts when trying to mint', async function () { - await expectRevertCustomError(this.token.$_mint(receiver, secondTokenId), 'EnforcedPause', []); + await expect(this.token.$_mint(this.receiver, otherTokenId)).to.be.revertedWithCustomError( + this.token, + 'EnforcedPause', + ); }); it('reverts when trying to burn', async function () { - await expectRevertCustomError(this.token.$_burn(firstTokenId), 'EnforcedPause', []); + await expect(this.token.$_burn(tokenId)).to.be.revertedWithCustomError(this.token, 'EnforcedPause'); }); describe('getApproved', function () { it('returns approved address', async function () { - const approvedAccount = await this.token.getApproved(firstTokenId); - expect(approvedAccount).to.equal(constants.ZERO_ADDRESS); + expect(await this.token.getApproved(tokenId)).to.equal(ethers.ZeroAddress); }); }); describe('balanceOf', function () { it('returns the amount of tokens owned by the given address', async function () { - const balance = await this.token.balanceOf(owner); - expect(balance).to.be.bignumber.equal('1'); + expect(await this.token.balanceOf(this.owner)).to.equal(1n); }); }); describe('ownerOf', function () { it('returns the amount of tokens owned by the given address', async function () { - const ownerOfToken = await this.token.ownerOf(firstTokenId); - expect(ownerOfToken).to.equal(owner); + expect(await this.token.ownerOf(tokenId)).to.equal(this.owner.address); }); }); describe('isApprovedForAll', function () { it('returns the approval of the operator', async function () { - expect(await this.token.isApprovedForAll(owner, operator)).to.equal(false); + expect(await this.token.isApprovedForAll(this.owner, this.operator)).to.be.false; }); }); }); diff --git a/test/token/ERC721/extensions/ERC721Royalty.test.js b/test/token/ERC721/extensions/ERC721Royalty.test.js index 78cba9858c6..e11954ae7a3 100644 --- a/test/token/ERC721/extensions/ERC721Royalty.test.js +++ b/test/token/ERC721/extensions/ERC721Royalty.test.js @@ -1,45 +1,55 @@ -require('@openzeppelin/test-helpers'); +const { ethers } = require('hardhat'); +const { expect } = require('chai'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); const { shouldBehaveLikeERC2981 } = require('../../common/ERC2981.behavior'); -const ERC721Royalty = artifacts.require('$ERC721Royalty'); +const name = 'Non Fungible Token'; +const symbol = 'NFT'; -contract('ERC721Royalty', function (accounts) { - const [account1, account2, recipient] = accounts; - const tokenId1 = web3.utils.toBN('1'); - const tokenId2 = web3.utils.toBN('2'); - const royalty = web3.utils.toBN('200'); - const salePrice = web3.utils.toBN('1000'); +const tokenId1 = 1n; +const tokenId2 = 2n; +const royalty = 200n; +const salePrice = 1000n; +async function fixture() { + const [account1, account2, recipient] = await ethers.getSigners(); + + const token = await ethers.deployContract('$ERC721Royalty', [name, symbol]); + await token.$_mint(account1, tokenId1); + await token.$_mint(account1, tokenId2); + + return { account1, account2, recipient, token }; +} + +describe('ERC721Royalty', function () { beforeEach(async function () { - this.token = await ERC721Royalty.new('My Token', 'TKN'); - - await this.token.$_mint(account1, tokenId1); - await this.token.$_mint(account1, tokenId2); - this.account1 = account1; - this.account2 = account2; - this.tokenId1 = tokenId1; - this.tokenId2 = tokenId2; - this.salePrice = salePrice; + Object.assign( + this, + await loadFixture(fixture), + { tokenId1, tokenId2, royalty, salePrice }, // set for behavior tests + ); }); describe('token specific functions', function () { beforeEach(async function () { - await this.token.$_setTokenRoyalty(tokenId1, recipient, royalty); + await this.token.$_setTokenRoyalty(tokenId1, this.recipient, royalty); }); it('royalty information are kept during burn and re-mint', async function () { await this.token.$_burn(tokenId1); - const tokenInfoA = await this.token.royaltyInfo(tokenId1, salePrice); - expect(tokenInfoA[0]).to.be.equal(recipient); - expect(tokenInfoA[1]).to.be.bignumber.equal(salePrice.mul(royalty).divn(1e4)); + expect(await this.token.royaltyInfo(tokenId1, salePrice)).to.deep.equal([ + this.recipient.address, + (salePrice * royalty) / 10000n, + ]); - await this.token.$_mint(account2, tokenId1); + await this.token.$_mint(this.account2, tokenId1); - const tokenInfoB = await this.token.royaltyInfo(tokenId1, salePrice); - expect(tokenInfoB[0]).to.be.equal(recipient); - expect(tokenInfoB[1]).to.be.bignumber.equal(salePrice.mul(royalty).divn(1e4)); + expect(await this.token.royaltyInfo(tokenId1, salePrice)).to.deep.equal([ + this.recipient.address, + (salePrice * royalty) / 10000n, + ]); }); }); diff --git a/test/token/ERC721/extensions/ERC721URIStorage.test.js b/test/token/ERC721/extensions/ERC721URIStorage.test.js index 8c882fab0a6..3a74f55ca4f 100644 --- a/test/token/ERC721/extensions/ERC721URIStorage.test.js +++ b/test/token/ERC721/extensions/ERC721URIStorage.test.js @@ -1,63 +1,64 @@ -const { BN, expectEvent } = require('@openzeppelin/test-helpers'); +const { ethers } = require('hardhat'); const { expect } = require('chai'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); const { shouldSupportInterfaces } = require('../../../utils/introspection/SupportsInterface.behavior'); -const { expectRevertCustomError } = require('../../../helpers/customError'); - -const ERC721URIStorageMock = artifacts.require('$ERC721URIStorageMock'); - -contract('ERC721URIStorage', function (accounts) { - const [owner] = accounts; - - const name = 'Non Fungible Token'; - const symbol = 'NFT'; - - const firstTokenId = new BN('5042'); - const nonExistentTokenId = new BN('13'); +const name = 'Non Fungible Token'; +const symbol = 'NFT'; +const baseURI = 'https://api.example.com/v1/'; +const otherBaseURI = 'https://api.example.com/v2/'; +const sampleUri = 'mock://mytoken'; +const tokenId = 1n; +const nonExistentTokenId = 2n; + +async function fixture() { + const [owner] = await ethers.getSigners(); + const token = await ethers.deployContract('$ERC721URIStorageMock', [name, symbol]); + return { owner, token }; +} + +contract('ERC721URIStorage', function () { beforeEach(async function () { - this.token = await ERC721URIStorageMock.new(name, symbol); + Object.assign(this, await loadFixture(fixture)); }); shouldSupportInterfaces(['0x49064906']); describe('token URI', function () { beforeEach(async function () { - await this.token.$_mint(owner, firstTokenId); + await this.token.$_mint(this.owner, tokenId); }); - const baseURI = 'https://api.example.com/v1/'; - const sampleUri = 'mock://mytoken'; - it('it is empty by default', async function () { - expect(await this.token.tokenURI(firstTokenId)).to.be.equal(''); + expect(await this.token.tokenURI(tokenId)).to.equal(''); }); it('reverts when queried for non existent token id', async function () { - await expectRevertCustomError(this.token.tokenURI(nonExistentTokenId), 'ERC721NonexistentToken', [ - nonExistentTokenId, - ]); + await expect(this.token.tokenURI(nonExistentTokenId)) + .to.be.revertedWithCustomError(this.token, 'ERC721NonexistentToken') + .withArgs(nonExistentTokenId); }); it('can be set for a token id', async function () { - await this.token.$_setTokenURI(firstTokenId, sampleUri); - expect(await this.token.tokenURI(firstTokenId)).to.be.equal(sampleUri); + await this.token.$_setTokenURI(tokenId, sampleUri); + expect(await this.token.tokenURI(tokenId)).to.equal(sampleUri); }); it('setting the uri emits an event', async function () { - expectEvent(await this.token.$_setTokenURI(firstTokenId, sampleUri), 'MetadataUpdate', { - _tokenId: firstTokenId, - }); + await expect(this.token.$_setTokenURI(tokenId, sampleUri)) + .to.emit(this.token, 'MetadataUpdate') + .withArgs(tokenId); }); it('setting the uri for non existent token id is allowed', async function () { - expectEvent(await this.token.$_setTokenURI(nonExistentTokenId, sampleUri), 'MetadataUpdate', { - _tokenId: nonExistentTokenId, - }); + await expect(await this.token.$_setTokenURI(nonExistentTokenId, sampleUri)) + .to.emit(this.token, 'MetadataUpdate') + .withArgs(nonExistentTokenId); // value will be accessible after mint - await this.token.$_mint(owner, nonExistentTokenId); - expect(await this.token.tokenURI(nonExistentTokenId)).to.be.equal(sampleUri); + await this.token.$_mint(this.owner, nonExistentTokenId); + expect(await this.token.tokenURI(nonExistentTokenId)).to.equal(sampleUri); }); it('base URI can be set', async function () { @@ -67,48 +68,54 @@ contract('ERC721URIStorage', function (accounts) { it('base URI is added as a prefix to the token URI', async function () { await this.token.setBaseURI(baseURI); - await this.token.$_setTokenURI(firstTokenId, sampleUri); + await this.token.$_setTokenURI(tokenId, sampleUri); - expect(await this.token.tokenURI(firstTokenId)).to.be.equal(baseURI + sampleUri); + expect(await this.token.tokenURI(tokenId)).to.equal(baseURI + sampleUri); }); it('token URI can be changed by changing the base URI', async function () { await this.token.setBaseURI(baseURI); - await this.token.$_setTokenURI(firstTokenId, sampleUri); + await this.token.$_setTokenURI(tokenId, sampleUri); - const newBaseURI = 'https://api.example.com/v2/'; - await this.token.setBaseURI(newBaseURI); - expect(await this.token.tokenURI(firstTokenId)).to.be.equal(newBaseURI + sampleUri); + await this.token.setBaseURI(otherBaseURI); + expect(await this.token.tokenURI(tokenId)).to.equal(otherBaseURI + sampleUri); }); it('tokenId is appended to base URI for tokens with no URI', async function () { await this.token.setBaseURI(baseURI); - expect(await this.token.tokenURI(firstTokenId)).to.be.equal(baseURI + firstTokenId); + expect(await this.token.tokenURI(tokenId)).to.equal(baseURI + tokenId); }); it('tokens without URI can be burnt ', async function () { - await this.token.$_burn(firstTokenId); + await this.token.$_burn(tokenId); - await expectRevertCustomError(this.token.tokenURI(firstTokenId), 'ERC721NonexistentToken', [firstTokenId]); + await expect(this.token.tokenURI(tokenId)) + .to.be.revertedWithCustomError(this.token, 'ERC721NonexistentToken') + .withArgs(tokenId); }); it('tokens with URI can be burnt ', async function () { - await this.token.$_setTokenURI(firstTokenId, sampleUri); + await this.token.$_setTokenURI(tokenId, sampleUri); - await this.token.$_burn(firstTokenId); + await this.token.$_burn(tokenId); - await expectRevertCustomError(this.token.tokenURI(firstTokenId), 'ERC721NonexistentToken', [firstTokenId]); + await expect(this.token.tokenURI(tokenId)) + .to.be.revertedWithCustomError(this.token, 'ERC721NonexistentToken') + .withArgs(tokenId); }); it('tokens URI is kept if token is burnt and reminted ', async function () { - await this.token.$_setTokenURI(firstTokenId, sampleUri); + await this.token.$_setTokenURI(tokenId, sampleUri); + + await this.token.$_burn(tokenId); - await this.token.$_burn(firstTokenId); - await expectRevertCustomError(this.token.tokenURI(firstTokenId), 'ERC721NonexistentToken', [firstTokenId]); + await expect(this.token.tokenURI(tokenId)) + .to.be.revertedWithCustomError(this.token, 'ERC721NonexistentToken') + .withArgs(tokenId); - await this.token.$_mint(owner, firstTokenId); - expect(await this.token.tokenURI(firstTokenId)).to.be.equal(sampleUri); + await this.token.$_mint(this.owner, tokenId); + expect(await this.token.tokenURI(tokenId)).to.equal(sampleUri); }); }); }); diff --git a/test/token/ERC721/extensions/ERC721Wrapper.test.js b/test/token/ERC721/extensions/ERC721Wrapper.test.js index 6839977449d..2c093a08723 100644 --- a/test/token/ERC721/extensions/ERC721Wrapper.test.js +++ b/test/token/ERC721/extensions/ERC721Wrapper.test.js @@ -1,26 +1,29 @@ -const { BN, expectEvent, constants } = require('@openzeppelin/test-helpers'); +const { ethers } = require('hardhat'); const { expect } = require('chai'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); const { shouldBehaveLikeERC721 } = require('../ERC721.behavior'); -const { expectRevertCustomError } = require('../../../helpers/customError'); -const ERC721 = artifacts.require('$ERC721'); -const ERC721Wrapper = artifacts.require('$ERC721Wrapper'); +const name = 'Non Fungible Token'; +const symbol = 'NFT'; +const tokenId = 1n; +const otherTokenId = 2n; -contract('ERC721Wrapper', function (accounts) { - const [initialHolder, anotherAccount, approvedAccount] = accounts; +async function fixture() { + const accounts = await ethers.getSigners(); + const [owner, approved, other] = accounts; - const name = 'My Token'; - const symbol = 'MTKN'; - const firstTokenId = new BN(1); - const secondTokenId = new BN(2); + const underlying = await ethers.deployContract('$ERC721', [name, symbol]); + await underlying.$_safeMint(owner, tokenId); + await underlying.$_safeMint(owner, otherTokenId); + const token = await ethers.deployContract('$ERC721Wrapper', [`Wrapped ${name}`, `W${symbol}`, underlying]); - beforeEach(async function () { - this.underlying = await ERC721.new(name, symbol); - this.token = await ERC721Wrapper.new(`Wrapped ${name}`, `W${symbol}`, this.underlying.address); + return { accounts, owner, approved, other, underlying, token }; +} - await this.underlying.$_safeMint(initialHolder, firstTokenId); - await this.underlying.$_safeMint(initialHolder, secondTokenId); +describe('ERC721Wrapper', function () { + beforeEach(async function () { + Object.assign(this, await loadFixture(fixture)); }); it('has a name', async function () { @@ -32,258 +35,167 @@ contract('ERC721Wrapper', function (accounts) { }); it('has underlying', async function () { - expect(await this.token.underlying()).to.be.bignumber.equal(this.underlying.address); + expect(await this.token.underlying()).to.equal(this.underlying.target); }); describe('depositFor', function () { it('works with token approval', async function () { - await this.underlying.approve(this.token.address, firstTokenId, { from: initialHolder }); - - const { tx } = await this.token.depositFor(initialHolder, [firstTokenId], { from: initialHolder }); - - await expectEvent.inTransaction(tx, this.underlying, 'Transfer', { - from: initialHolder, - to: this.token.address, - tokenId: firstTokenId, - }); - await expectEvent.inTransaction(tx, this.token, 'Transfer', { - from: constants.ZERO_ADDRESS, - to: initialHolder, - tokenId: firstTokenId, - }); + await this.underlying.connect(this.owner).approve(this.token, tokenId); + + await expect(this.token.connect(this.owner).depositFor(this.owner, [tokenId])) + .to.emit(this.underlying, 'Transfer') + .withArgs(this.owner.address, this.token.target, tokenId) + .to.emit(this.token, 'Transfer') + .withArgs(ethers.ZeroAddress, this.owner.address, tokenId); }); it('works with approval for all', async function () { - await this.underlying.setApprovalForAll(this.token.address, true, { from: initialHolder }); - - const { tx } = await this.token.depositFor(initialHolder, [firstTokenId], { from: initialHolder }); - - await expectEvent.inTransaction(tx, this.underlying, 'Transfer', { - from: initialHolder, - to: this.token.address, - tokenId: firstTokenId, - }); - await expectEvent.inTransaction(tx, this.token, 'Transfer', { - from: constants.ZERO_ADDRESS, - to: initialHolder, - tokenId: firstTokenId, - }); + await this.underlying.connect(this.owner).setApprovalForAll(this.token, true); + + await expect(this.token.connect(this.owner).depositFor(this.owner, [tokenId])) + .to.emit(this.underlying, 'Transfer') + .withArgs(this.owner.address, this.token.target, tokenId) + .to.emit(this.token, 'Transfer') + .withArgs(ethers.ZeroAddress, this.owner.address, tokenId); }); it('works sending to another account', async function () { - await this.underlying.approve(this.token.address, firstTokenId, { from: initialHolder }); - - const { tx } = await this.token.depositFor(anotherAccount, [firstTokenId], { from: initialHolder }); - - await expectEvent.inTransaction(tx, this.underlying, 'Transfer', { - from: initialHolder, - to: this.token.address, - tokenId: firstTokenId, - }); - await expectEvent.inTransaction(tx, this.token, 'Transfer', { - from: constants.ZERO_ADDRESS, - to: anotherAccount, - tokenId: firstTokenId, - }); + await this.underlying.connect(this.owner).approve(this.token, tokenId); + + await expect(this.token.connect(this.owner).depositFor(this.other, [tokenId])) + .to.emit(this.underlying, 'Transfer') + .withArgs(this.owner.address, this.token.target, tokenId) + .to.emit(this.token, 'Transfer') + .withArgs(ethers.ZeroAddress, this.other.address, tokenId); }); it('works with multiple tokens', async function () { - await this.underlying.approve(this.token.address, firstTokenId, { from: initialHolder }); - await this.underlying.approve(this.token.address, secondTokenId, { from: initialHolder }); - - const { tx } = await this.token.depositFor(initialHolder, [firstTokenId, secondTokenId], { from: initialHolder }); - - await expectEvent.inTransaction(tx, this.underlying, 'Transfer', { - from: initialHolder, - to: this.token.address, - tokenId: firstTokenId, - }); - await expectEvent.inTransaction(tx, this.token, 'Transfer', { - from: constants.ZERO_ADDRESS, - to: initialHolder, - tokenId: firstTokenId, - }); - await expectEvent.inTransaction(tx, this.underlying, 'Transfer', { - from: initialHolder, - to: this.token.address, - tokenId: secondTokenId, - }); - await expectEvent.inTransaction(tx, this.token, 'Transfer', { - from: constants.ZERO_ADDRESS, - to: initialHolder, - tokenId: secondTokenId, - }); + await this.underlying.connect(this.owner).approve(this.token, tokenId); + await this.underlying.connect(this.owner).approve(this.token, otherTokenId); + + await expect(this.token.connect(this.owner).depositFor(this.owner, [tokenId, otherTokenId])) + .to.emit(this.underlying, 'Transfer') + .withArgs(this.owner.address, this.token.target, tokenId) + .to.emit(this.token, 'Transfer') + .withArgs(ethers.ZeroAddress, this.owner.address, tokenId) + .to.emit(this.underlying, 'Transfer') + .withArgs(this.owner.address, this.token.target, otherTokenId) + .to.emit(this.token, 'Transfer') + .withArgs(ethers.ZeroAddress, this.owner.address, otherTokenId); }); it('reverts with missing approval', async function () { - await expectRevertCustomError( - this.token.depositFor(initialHolder, [firstTokenId], { from: initialHolder }), - 'ERC721InsufficientApproval', - [this.token.address, firstTokenId], - ); + await expect(this.token.connect(this.owner).depositFor(this.owner, [tokenId])) + .to.be.revertedWithCustomError(this.token, 'ERC721InsufficientApproval') + .withArgs(this.token.target, tokenId); }); }); describe('withdrawTo', function () { beforeEach(async function () { - await this.underlying.approve(this.token.address, firstTokenId, { from: initialHolder }); - await this.token.depositFor(initialHolder, [firstTokenId], { from: initialHolder }); + await this.underlying.connect(this.owner).approve(this.token, tokenId); + await this.token.connect(this.owner).depositFor(this.owner, [tokenId]); }); it('works for an owner', async function () { - const { tx } = await this.token.withdrawTo(initialHolder, [firstTokenId], { from: initialHolder }); - - await expectEvent.inTransaction(tx, this.underlying, 'Transfer', { - from: this.token.address, - to: initialHolder, - tokenId: firstTokenId, - }); - await expectEvent.inTransaction(tx, this.token, 'Transfer', { - from: initialHolder, - to: constants.ZERO_ADDRESS, - tokenId: firstTokenId, - }); + await expect(this.token.connect(this.owner).withdrawTo(this.owner, [tokenId])) + .to.emit(this.underlying, 'Transfer') + .withArgs(this.token.target, this.owner.address, tokenId) + .to.emit(this.token, 'Transfer') + .withArgs(this.owner.address, ethers.ZeroAddress, tokenId); }); it('works for an approved', async function () { - await this.token.approve(approvedAccount, firstTokenId, { from: initialHolder }); - - const { tx } = await this.token.withdrawTo(initialHolder, [firstTokenId], { from: approvedAccount }); - - await expectEvent.inTransaction(tx, this.underlying, 'Transfer', { - from: this.token.address, - to: initialHolder, - tokenId: firstTokenId, - }); - await expectEvent.inTransaction(tx, this.token, 'Transfer', { - from: initialHolder, - to: constants.ZERO_ADDRESS, - tokenId: firstTokenId, - }); + await this.token.connect(this.owner).approve(this.approved, tokenId); + + await expect(this.token.connect(this.approved).withdrawTo(this.owner, [tokenId])) + .to.emit(this.underlying, 'Transfer') + .withArgs(this.token.target, this.owner.address, tokenId) + .to.emit(this.token, 'Transfer') + .withArgs(this.owner.address, ethers.ZeroAddress, tokenId); }); it('works for an approved for all', async function () { - await this.token.setApprovalForAll(approvedAccount, true, { from: initialHolder }); - - const { tx } = await this.token.withdrawTo(initialHolder, [firstTokenId], { from: approvedAccount }); - - await expectEvent.inTransaction(tx, this.underlying, 'Transfer', { - from: this.token.address, - to: initialHolder, - tokenId: firstTokenId, - }); - await expectEvent.inTransaction(tx, this.token, 'Transfer', { - from: initialHolder, - to: constants.ZERO_ADDRESS, - tokenId: firstTokenId, - }); + await this.token.connect(this.owner).setApprovalForAll(this.approved, true); + + await expect(this.token.connect(this.approved).withdrawTo(this.owner, [tokenId])) + .to.emit(this.underlying, 'Transfer') + .withArgs(this.token.target, this.owner.address, tokenId) + .to.emit(this.token, 'Transfer') + .withArgs(this.owner.address, ethers.ZeroAddress, tokenId); }); it("doesn't work for a non-owner nor approved", async function () { - await expectRevertCustomError( - this.token.withdrawTo(initialHolder, [firstTokenId], { from: anotherAccount }), - 'ERC721InsufficientApproval', - [anotherAccount, firstTokenId], - ); + await expect(this.token.connect(this.other).withdrawTo(this.owner, [tokenId])) + .to.be.revertedWithCustomError(this.token, 'ERC721InsufficientApproval') + .withArgs(this.other.address, tokenId); }); it('works with multiple tokens', async function () { - await this.underlying.approve(this.token.address, secondTokenId, { from: initialHolder }); - await this.token.depositFor(initialHolder, [secondTokenId], { from: initialHolder }); - - const { tx } = await this.token.withdrawTo(initialHolder, [firstTokenId, secondTokenId], { from: initialHolder }); - - await expectEvent.inTransaction(tx, this.underlying, 'Transfer', { - from: this.token.address, - to: initialHolder, - tokenId: firstTokenId, - }); - await expectEvent.inTransaction(tx, this.underlying, 'Transfer', { - from: this.token.address, - to: initialHolder, - tokenId: secondTokenId, - }); - await expectEvent.inTransaction(tx, this.token, 'Transfer', { - from: initialHolder, - to: constants.ZERO_ADDRESS, - tokenId: firstTokenId, - }); - await expectEvent.inTransaction(tx, this.token, 'Transfer', { - from: initialHolder, - to: constants.ZERO_ADDRESS, - tokenId: secondTokenId, - }); + await this.underlying.connect(this.owner).approve(this.token, otherTokenId); + await this.token.connect(this.owner).depositFor(this.owner, [otherTokenId]); + + await expect(this.token.connect(this.owner).withdrawTo(this.owner, [tokenId, otherTokenId])) + .to.emit(this.underlying, 'Transfer') + .withArgs(this.token.target, this.owner.address, tokenId) + .to.emit(this.underlying, 'Transfer') + .withArgs(this.token.target, this.owner.address, tokenId) + .to.emit(this.token, 'Transfer') + .withArgs(this.owner.address, ethers.ZeroAddress, tokenId) + .to.emit(this.token, 'Transfer') + .withArgs(this.owner.address, ethers.ZeroAddress, tokenId); }); it('works to another account', async function () { - const { tx } = await this.token.withdrawTo(anotherAccount, [firstTokenId], { from: initialHolder }); - - await expectEvent.inTransaction(tx, this.underlying, 'Transfer', { - from: this.token.address, - to: anotherAccount, - tokenId: firstTokenId, - }); - await expectEvent.inTransaction(tx, this.token, 'Transfer', { - from: initialHolder, - to: constants.ZERO_ADDRESS, - tokenId: firstTokenId, - }); + await expect(this.token.connect(this.owner).withdrawTo(this.other, [tokenId])) + .to.emit(this.underlying, 'Transfer') + .withArgs(this.token.target, this.other.address, tokenId) + .to.emit(this.token, 'Transfer') + .withArgs(this.owner.address, ethers.ZeroAddress, tokenId); }); }); describe('onERC721Received', function () { it('only allows calls from underlying', async function () { - await expectRevertCustomError( - this.token.onERC721Received( - initialHolder, - this.token.address, - firstTokenId, - anotherAccount, // Correct data - { from: anotherAccount }, + await expect( + this.token.connect(this.other).onERC721Received( + this.owner, + this.token, + tokenId, + this.other.address, // Correct data ), - 'ERC721UnsupportedToken', - [anotherAccount], - ); + ) + .to.be.revertedWithCustomError(this.token, 'ERC721UnsupportedToken') + .withArgs(this.other.address); }); it('mints a token to from', async function () { - const { tx } = await this.underlying.safeTransferFrom(initialHolder, this.token.address, firstTokenId, { - from: initialHolder, - }); - - await expectEvent.inTransaction(tx, this.token, 'Transfer', { - from: constants.ZERO_ADDRESS, - to: initialHolder, - tokenId: firstTokenId, - }); + await expect(this.underlying.connect(this.owner).safeTransferFrom(this.owner, this.token, tokenId)) + .to.emit(this.token, 'Transfer') + .withArgs(ethers.ZeroAddress, this.owner.address, tokenId); }); }); describe('_recover', function () { it('works if there is something to recover', async function () { // Should use `transferFrom` to avoid `onERC721Received` minting - await this.underlying.transferFrom(initialHolder, this.token.address, firstTokenId, { from: initialHolder }); + await this.underlying.connect(this.owner).transferFrom(this.owner, this.token, tokenId); - const { tx } = await this.token.$_recover(anotherAccount, firstTokenId); - - await expectEvent.inTransaction(tx, this.token, 'Transfer', { - from: constants.ZERO_ADDRESS, - to: anotherAccount, - tokenId: firstTokenId, - }); + await expect(this.token.$_recover(this.other, tokenId)) + .to.emit(this.token, 'Transfer') + .withArgs(ethers.ZeroAddress, this.other.address, tokenId); }); it('reverts if there is nothing to recover', async function () { - const owner = await this.underlying.ownerOf(firstTokenId); - await expectRevertCustomError(this.token.$_recover(initialHolder, firstTokenId), 'ERC721IncorrectOwner', [ - this.token.address, - firstTokenId, - owner, - ]); + const holder = await this.underlying.ownerOf(tokenId); + + await expect(this.token.$_recover(holder, tokenId)) + .to.be.revertedWithCustomError(this.token, 'ERC721IncorrectOwner') + .withArgs(this.token.target, tokenId, holder); }); }); describe('ERC712 behavior', function () { - shouldBehaveLikeERC721(...accounts); + shouldBehaveLikeERC721(); }); }); diff --git a/test/token/ERC721/utils/ERC721Holder.test.js b/test/token/ERC721/utils/ERC721Holder.test.js index 4aa2b79484b..01b774930ab 100644 --- a/test/token/ERC721/utils/ERC721Holder.test.js +++ b/test/token/ERC721/utils/ERC721Holder.test.js @@ -1,22 +1,20 @@ +const { ethers } = require('hardhat'); const { expect } = require('chai'); -const ERC721Holder = artifacts.require('$ERC721Holder'); -const ERC721 = artifacts.require('$ERC721'); - -contract('ERC721Holder', function (accounts) { - const [owner] = accounts; - - const name = 'Non Fungible Token'; - const symbol = 'NFT'; - const tokenId = web3.utils.toBN(1); +const name = 'Non Fungible Token'; +const symbol = 'NFT'; +const tokenId = 1n; +describe('ERC721Holder', function () { it('receives an ERC721 token', async function () { - const token = await ERC721.new(name, symbol); + const [owner] = await ethers.getSigners(); + + const token = await ethers.deployContract('$ERC721', [name, symbol]); await token.$_mint(owner, tokenId); - const receiver = await ERC721Holder.new(); - await token.safeTransferFrom(owner, receiver.address, tokenId, { from: owner }); + const receiver = await ethers.deployContract('$ERC721Holder'); + await token.connect(owner).safeTransferFrom(owner, receiver, tokenId); - expect(await token.ownerOf(tokenId)).to.be.equal(receiver.address); + expect(await token.ownerOf(tokenId)).to.equal(receiver.target); }); }); diff --git a/test/token/common/ERC2981.behavior.js b/test/token/common/ERC2981.behavior.js index 1c062b0524c..ae6abccaedb 100644 --- a/test/token/common/ERC2981.behavior.js +++ b/test/token/common/ERC2981.behavior.js @@ -1,12 +1,10 @@ -const { BN, constants } = require('@openzeppelin/test-helpers'); +const { ethers } = require('hardhat'); const { expect } = require('chai'); -const { ZERO_ADDRESS } = constants; const { shouldSupportInterfaces } = require('../../utils/introspection/SupportsInterface.behavior'); -const { expectRevertCustomError } = require('../../helpers/customError'); function shouldBehaveLikeERC2981() { - const royaltyFraction = new BN('10'); + const royaltyFraction = 10n; shouldSupportInterfaces(['ERC2981']); @@ -16,64 +14,54 @@ function shouldBehaveLikeERC2981() { }); it('checks royalty is set', async function () { - const royalty = new BN((this.salePrice * royaltyFraction) / 10000); - - const initInfo = await this.token.royaltyInfo(this.tokenId1, this.salePrice); - - expect(initInfo[0]).to.be.equal(this.account1); - expect(initInfo[1]).to.be.bignumber.equal(royalty); + expect(await this.token.royaltyInfo(this.tokenId1, this.salePrice)).to.deep.equal([ + this.account1.address, + (this.salePrice * royaltyFraction) / 10_000n, + ]); }); it('updates royalty amount', async function () { - const newPercentage = new BN('25'); + const newFraction = 25n; - // Updated royalty check - await this.token.$_setDefaultRoyalty(this.account1, newPercentage); - const royalty = new BN((this.salePrice * newPercentage) / 10000); - const newInfo = await this.token.royaltyInfo(this.tokenId1, this.salePrice); + await this.token.$_setDefaultRoyalty(this.account1, newFraction); - expect(newInfo[0]).to.be.equal(this.account1); - expect(newInfo[1]).to.be.bignumber.equal(royalty); + expect(await this.token.royaltyInfo(this.tokenId1, this.salePrice)).to.deep.equal([ + this.account1.address, + (this.salePrice * newFraction) / 10_000n, + ]); }); it('holds same royalty value for different tokens', async function () { - const newPercentage = new BN('20'); - await this.token.$_setDefaultRoyalty(this.account1, newPercentage); + const newFraction = 20n; - const token1Info = await this.token.royaltyInfo(this.tokenId1, this.salePrice); - const token2Info = await this.token.royaltyInfo(this.tokenId2, this.salePrice); + await this.token.$_setDefaultRoyalty(this.account1, newFraction); - expect(token1Info[1]).to.be.bignumber.equal(token2Info[1]); + expect(await this.token.royaltyInfo(this.tokenId1, this.salePrice)).to.deep.equal( + await this.token.royaltyInfo(this.tokenId2, this.salePrice), + ); }); it('Remove royalty information', async function () { - const newValue = new BN('0'); + const newValue = 0n; await this.token.$_deleteDefaultRoyalty(); - const token1Info = await this.token.royaltyInfo(this.tokenId1, this.salePrice); - const token2Info = await this.token.royaltyInfo(this.tokenId2, this.salePrice); - // Test royalty info is still persistent across all tokens - expect(token1Info[0]).to.be.bignumber.equal(token2Info[0]); - expect(token1Info[1]).to.be.bignumber.equal(token2Info[1]); - // Test information was deleted - expect(token1Info[0]).to.be.equal(ZERO_ADDRESS); - expect(token1Info[1]).to.be.bignumber.equal(newValue); + expect(await this.token.royaltyInfo(this.tokenId1, this.salePrice)).to.deep.equal([ethers.ZeroAddress, newValue]); + + expect(await this.token.royaltyInfo(this.tokenId2, this.salePrice)).to.deep.equal([ethers.ZeroAddress, newValue]); }); it('reverts if invalid parameters', async function () { const royaltyDenominator = await this.token.$_feeDenominator(); - await expectRevertCustomError( - this.token.$_setDefaultRoyalty(ZERO_ADDRESS, royaltyFraction), - 'ERC2981InvalidDefaultRoyaltyReceiver', - [ZERO_ADDRESS], - ); - const anotherRoyaltyFraction = new BN('11000'); - await expectRevertCustomError( - this.token.$_setDefaultRoyalty(this.account1, anotherRoyaltyFraction), - 'ERC2981InvalidDefaultRoyalty', - [anotherRoyaltyFraction, royaltyDenominator], - ); + await expect(this.token.$_setDefaultRoyalty(ethers.ZeroAddress, royaltyFraction)) + .to.be.revertedWithCustomError(this.token, 'ERC2981InvalidDefaultRoyaltyReceiver') + .withArgs(ethers.ZeroAddress); + + const anotherRoyaltyFraction = 11000n; + + await expect(this.token.$_setDefaultRoyalty(this.account1, anotherRoyaltyFraction)) + .to.be.revertedWithCustomError(this.token, 'ERC2981InvalidDefaultRoyalty') + .withArgs(anotherRoyaltyFraction, royaltyDenominator); }); }); @@ -83,83 +71,78 @@ function shouldBehaveLikeERC2981() { }); it('updates royalty amount', async function () { - const newPercentage = new BN('25'); - let royalty = new BN((this.salePrice * royaltyFraction) / 10000); - // Initial royalty check - const initInfo = await this.token.royaltyInfo(this.tokenId1, this.salePrice); + const newFraction = 25n; - expect(initInfo[0]).to.be.equal(this.account1); - expect(initInfo[1]).to.be.bignumber.equal(royalty); + expect(await this.token.royaltyInfo(this.tokenId1, this.salePrice)).to.deep.equal([ + this.account1.address, + (this.salePrice * royaltyFraction) / 10_000n, + ]); - // Updated royalty check - await this.token.$_setTokenRoyalty(this.tokenId1, this.account1, newPercentage); - royalty = new BN((this.salePrice * newPercentage) / 10000); - const newInfo = await this.token.royaltyInfo(this.tokenId1, this.salePrice); + await this.token.$_setTokenRoyalty(this.tokenId1, this.account1, newFraction); - expect(newInfo[0]).to.be.equal(this.account1); - expect(newInfo[1]).to.be.bignumber.equal(royalty); + expect(await this.token.royaltyInfo(this.tokenId1, this.salePrice)).to.deep.equal([ + this.account1.address, + (this.salePrice * newFraction) / 10_000n, + ]); }); it('holds different values for different tokens', async function () { - const newPercentage = new BN('20'); - await this.token.$_setTokenRoyalty(this.tokenId2, this.account1, newPercentage); + const newFraction = 20n; - const token1Info = await this.token.royaltyInfo(this.tokenId1, this.salePrice); - const token2Info = await this.token.royaltyInfo(this.tokenId2, this.salePrice); + await this.token.$_setTokenRoyalty(this.tokenId2, this.account1, newFraction); - // must be different even at the same this.salePrice - expect(token1Info[1]).to.not.be.bignumber.equal(token2Info[1]); + expect(await this.token.royaltyInfo(this.tokenId1, this.salePrice)).to.not.deep.equal( + await this.token.royaltyInfo(this.tokenId2, this.salePrice), + ); }); it('reverts if invalid parameters', async function () { const royaltyDenominator = await this.token.$_feeDenominator(); - await expectRevertCustomError( - this.token.$_setTokenRoyalty(this.tokenId1, ZERO_ADDRESS, royaltyFraction), - 'ERC2981InvalidTokenRoyaltyReceiver', - [this.tokenId1.toString(), ZERO_ADDRESS], - ); - const anotherRoyaltyFraction = new BN('11000'); - await expectRevertCustomError( - this.token.$_setTokenRoyalty(this.tokenId1, this.account1, anotherRoyaltyFraction), - 'ERC2981InvalidTokenRoyalty', - [this.tokenId1.toString(), anotherRoyaltyFraction, royaltyDenominator], - ); + await expect(this.token.$_setTokenRoyalty(this.tokenId1, ethers.ZeroAddress, royaltyFraction)) + .to.be.revertedWithCustomError(this.token, 'ERC2981InvalidTokenRoyaltyReceiver') + .withArgs(this.tokenId1, ethers.ZeroAddress); + + const anotherRoyaltyFraction = 11000n; + + await expect(this.token.$_setTokenRoyalty(this.tokenId1, this.account1, anotherRoyaltyFraction)) + .to.be.revertedWithCustomError(this.token, 'ERC2981InvalidTokenRoyalty') + .withArgs(this.tokenId1, anotherRoyaltyFraction, royaltyDenominator); }); it('can reset token after setting royalty', async function () { - const newPercentage = new BN('30'); - const royalty = new BN((this.salePrice * newPercentage) / 10000); - await this.token.$_setTokenRoyalty(this.tokenId1, this.account2, newPercentage); + const newFraction = 30n; - const tokenInfo = await this.token.royaltyInfo(this.tokenId1, this.salePrice); + await this.token.$_setTokenRoyalty(this.tokenId1, this.account2, newFraction); // Tokens must have own information - expect(tokenInfo[1]).to.be.bignumber.equal(royalty); - expect(tokenInfo[0]).to.be.equal(this.account2); + expect(await this.token.royaltyInfo(this.tokenId1, this.salePrice)).to.deep.equal([ + this.account2.address, + (this.salePrice * newFraction) / 10_000n, + ]); + + await this.token.$_setTokenRoyalty(this.tokenId2, this.account1, 0n); - await this.token.$_setTokenRoyalty(this.tokenId2, this.account1, new BN('0')); - const result = await this.token.royaltyInfo(this.tokenId2, this.salePrice); // Token must not share default information - expect(result[0]).to.be.equal(this.account1); - expect(result[1]).to.be.bignumber.equal(new BN('0')); + expect(await this.token.royaltyInfo(this.tokenId2, this.salePrice)).to.deep.equal([this.account1.address, 0n]); }); it('can hold default and token royalty information', async function () { - const newPercentage = new BN('30'); - const royalty = new BN((this.salePrice * newPercentage) / 10000); + const newFraction = 30n; - await this.token.$_setTokenRoyalty(this.tokenId2, this.account2, newPercentage); + await this.token.$_setTokenRoyalty(this.tokenId2, this.account2, newFraction); - const token1Info = await this.token.royaltyInfo(this.tokenId1, this.salePrice); - const token2Info = await this.token.royaltyInfo(this.tokenId2, this.salePrice); // Tokens must not have same values - expect(token1Info[1]).to.not.be.bignumber.equal(token2Info[1]); - expect(token1Info[0]).to.not.be.equal(token2Info[0]); + expect(await this.token.royaltyInfo(this.tokenId1, this.salePrice)).to.not.deep.equal([ + this.account2.address, + (this.salePrice * newFraction) / 10_000n, + ]); // Updated token must have new values - expect(token2Info[0]).to.be.equal(this.account2); - expect(token2Info[1]).to.be.bignumber.equal(royalty); + expect(await this.token.royaltyInfo(this.tokenId2, this.salePrice)).to.deep.equal([ + this.account2.address, + (this.salePrice * newFraction) / 10_000n, + ]); }); }); }