Use the _update mechanism in ERC721 (#4377)

Co-authored-by: Francisco Giordano <fg@frang.io>
Co-authored-by: Ernesto García <ernestognw@gmail.com>
This commit is contained in:
Hadrien Croubois
2023-08-09 19:03:27 +02:00
committed by GitHub
parent 8643fd45fd
commit 9e3f4d60c5
19 changed files with 466 additions and 501 deletions

View File

@ -104,7 +104,7 @@ function shouldBehaveLikeERC721(owner, newOwner, approved, anotherApproved, oper
});
};
const shouldTransferTokensByUsers = function (transferFunction) {
const shouldTransferTokensByUsers = function (transferFunction, opts = {}) {
context('when called by the owner', function () {
beforeEach(async function () {
receipt = await transferFunction.call(this, owner, this.toWhom, tokenId, { from: owner });
@ -180,13 +180,19 @@ function shouldBehaveLikeERC721(owner, newOwner, approved, anotherApproved, oper
});
context('when the sender is not authorized for the token id', function () {
it('reverts', async function () {
await expectRevertCustomError(
transferFunction.call(this, owner, other, tokenId, { from: other }),
'ERC721InsufficientApproval',
[other, tokenId],
);
});
if (opts.unrestricted) {
it('does not revert', async function () {
await transferFunction.call(this, owner, other, tokenId, { from: other });
});
} else {
it('reverts', async function () {
await expectRevertCustomError(
transferFunction.call(this, owner, other, tokenId, { from: other }),
'ERC721InsufficientApproval',
[other, tokenId],
);
});
}
});
context('when the given token ID does not exist', function () {
@ -210,145 +216,170 @@ function shouldBehaveLikeERC721(owner, newOwner, approved, anotherApproved, oper
});
};
describe('via transferFrom', function () {
shouldTransferTokensByUsers(function (from, to, tokenId, opts) {
return this.token.transferFrom(from, to, tokenId, opts);
const shouldTransferSafely = function (transferFun, data, opts = {}) {
describe('to a user account', function () {
shouldTransferTokensByUsers(transferFun, opts);
});
});
describe('via safeTransferFrom', function () {
const safeTransferFromWithData = function (from, to, tokenId, opts) {
return this.token.methods['safeTransferFrom(address,address,uint256,bytes)'](from, to, tokenId, data, opts);
};
const safeTransferFromWithoutData = function (from, to, tokenId, opts) {
return this.token.methods['safeTransferFrom(address,address,uint256)'](from, to, tokenId, opts);
};
const shouldTransferSafely = function (transferFun, data) {
describe('to a user account', function () {
shouldTransferTokensByUsers(transferFun);
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;
});
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;
});
shouldTransferTokensByUsers(transferFun, opts);
shouldTransferTokensByUsers(transferFun);
it('calls onERC721Received', async function () {
const receipt = await transferFun.call(this, owner, this.receiver.address, tokenId, { from: owner });
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,
});
});
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,
});
});
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 expectEvent.inTransaction(receipt.tx, ERC721ReceiverMock, 'Received', {
operator: owner,
from: owner,
tokenId: tokenId,
data: data,
});
});
};
describe('with data', function () {
shouldTransferSafely(safeTransferFromWithData, data);
});
it('calls onERC721Received from approved', async function () {
const receipt = await transferFun.call(this, owner, this.receiver.address, tokenId, { from: approved });
describe('without data', function () {
shouldTransferSafely(safeTransferFromWithoutData, null);
});
await expectEvent.inTransaction(receipt.tx, ERC721ReceiverMock, 'Received', {
operator: approved,
from: owner,
tokenId: tokenId,
data: data,
});
});
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.safeTransferFrom(owner, invalidReceiver.address, tokenId, { from: owner }),
'ERC721InvalidReceiver',
[invalidReceiver.address],
);
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],
);
});
});
});
};
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.safeTransferFrom(owner, revertingReceiver.address, tokenId, { from: owner }),
'ERC721ReceiverMock: reverting',
);
for (const { fnName, opts } of [
{ fnName: 'transferFrom', opts: {} },
{ fnName: '$_transfer', opts: { unrestricted: true } },
]) {
describe(`via ${fnName}`, function () {
shouldTransferTokensByUsers(function (from, to, tokenId, opts) {
return this.token[fnName](from, to, tokenId, opts);
}, opts);
});
}
for (const { fnName, opts } of [
{ fnName: 'safeTransferFrom', opts: {} },
{ 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);
});
describe('without data', function () {
shouldTransferSafely(safeTransferFromWithoutData, null, 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],
);
});
});
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.methods[fnName + '(address,address,uint256)'](owner, revertingReceiver.address, tokenId, {
from: owner,
}),
'ERC721ReceiverMock: reverting',
);
});
});
describe('to a receiver contract that reverts without message', function () {
it('reverts', async function () {
const revertingReceiver = await ERC721ReceiverMock.new(
RECEIVER_MAGIC_VALUE,
RevertType.RevertWithoutMessage,
);
await expectRevertCustomError(
this.token.methods[fnName + '(address,address,uint256)'](owner, revertingReceiver.address, tokenId, {
from: owner,
}),
'ERC721InvalidReceiver',
[revertingReceiver.address],
);
});
});
describe('to a receiver contract that reverts with custom error', function () {
it('reverts', async function () {
const revertingReceiver = await ERC721ReceiverMock.new(
RECEIVER_MAGIC_VALUE,
RevertType.RevertWithCustomError,
);
await expectRevertCustomError(
this.token.methods[fnName + '(address,address,uint256)'](owner, revertingReceiver.address, tokenId, {
from: owner,
}),
'CustomError',
[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,
}),
);
});
});
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],
);
});
});
});
describe('to a receiver contract that reverts without message', function () {
it('reverts', async function () {
const revertingReceiver = await ERC721ReceiverMock.new(
RECEIVER_MAGIC_VALUE,
RevertType.RevertWithoutMessage,
);
await expectRevertCustomError(
this.token.safeTransferFrom(owner, revertingReceiver.address, tokenId, { from: owner }),
'ERC721InvalidReceiver',
[revertingReceiver.address],
);
});
});
describe('to a receiver contract that reverts with custom error', function () {
it('reverts', async function () {
const revertingReceiver = await ERC721ReceiverMock.new(
RECEIVER_MAGIC_VALUE,
RevertType.RevertWithCustomError,
);
await expectRevertCustomError(
this.token.safeTransferFrom(owner, revertingReceiver.address, tokenId, { from: owner }),
'CustomError',
[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.safeTransferFrom(owner, revertingReceiver.address, tokenId, { from: owner }),
);
});
});
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.safeTransferFrom(owner, nonReceiver.address, tokenId, { from: owner }),
'ERC721InvalidReceiver',
[nonReceiver.address],
);
});
});
});
}
});
describe('safe mint', function () {
@ -524,14 +555,6 @@ function shouldBehaveLikeERC721(owner, newOwner, approved, anotherApproved, oper
});
});
context('when the address that receives the approval is the owner', function () {
it('reverts', async function () {
await expectRevertCustomError(this.token.approve(owner, tokenId, { from: owner }), 'ERC721InvalidOperator', [
owner,
]);
});
});
context('when the sender does not own the given token ID', function () {
it('reverts', async function () {
await expectRevertCustomError(
@ -645,12 +668,12 @@ function shouldBehaveLikeERC721(owner, newOwner, approved, anotherApproved, oper
});
});
context('when the operator is the owner', function () {
context('when the operator is address zero', function () {
it('reverts', async function () {
await expectRevertCustomError(
this.token.setApprovalForAll(owner, true, { from: owner }),
this.token.setApprovalForAll(constants.ZERO_ADDRESS, true, { from: owner }),
'ERC721InvalidOperator',
[owner],
[constants.ZERO_ADDRESS],
);
});
});

View File

@ -103,7 +103,7 @@ contract('ERC721Consecutive', function (accounts) {
it('simple minting is possible after construction', async function () {
const tokenId = sum(...batches.map(b => b.amount)) + offset;
expect(await this.token.$_exists(tokenId)).to.be.equal(false);
await expectRevertCustomError(this.token.ownerOf(tokenId), 'ERC721NonexistentToken', [tokenId]);
expectEvent(await this.token.$_mint(user1, tokenId), 'Transfer', {
from: constants.ZERO_ADDRESS,
@ -115,7 +115,7 @@ contract('ERC721Consecutive', function (accounts) {
it('cannot mint a token that has been batched minted', async function () {
const tokenId = sum(...batches.map(b => b.amount)) + offset - 1;
expect(await this.token.$_exists(tokenId)).to.be.equal(true);
expect(await this.token.ownerOf(tokenId)).to.be.not.equal(constants.ZERO_ADDRESS);
await expectRevertCustomError(this.token.$_mint(user1, tokenId), 'ERC721InvalidSender', [ZERO_ADDRESS]);
});
@ -151,13 +151,11 @@ contract('ERC721Consecutive', function (accounts) {
it('tokens can be burned and re-minted #2', async function () {
const tokenId = web3.utils.toBN(sum(...batches.map(({ amount }) => amount)) + offset);
expect(await this.token.$_exists(tokenId)).to.be.equal(false);
await expectRevertCustomError(this.token.ownerOf(tokenId), 'ERC721NonexistentToken', [tokenId]);
// mint
await this.token.$_mint(user1, tokenId);
expect(await this.token.$_exists(tokenId)).to.be.equal(true);
expect(await this.token.ownerOf(tokenId), user1);
// burn
@ -167,7 +165,6 @@ contract('ERC721Consecutive', function (accounts) {
tokenId,
});
expect(await this.token.$_exists(tokenId)).to.be.equal(false);
await expectRevertCustomError(this.token.ownerOf(tokenId), 'ERC721NonexistentToken', [tokenId]);
// re-mint
@ -177,20 +174,8 @@ contract('ERC721Consecutive', function (accounts) {
tokenId,
});
expect(await this.token.$_exists(tokenId)).to.be.equal(true);
expect(await this.token.ownerOf(tokenId), user2);
});
it('reverts burning batches of size != 1', async function () {
const tokenId = batches[0].amount + offset;
const receiver = batches[0].receiver;
await expectRevertCustomError(
this.token.$_afterTokenTransfer(receiver, ZERO_ADDRESS, tokenId, 2),
'ERC721ForbiddenBatchBurn',
[],
);
});
});
});
}

View File

@ -81,12 +81,6 @@ contract('ERC721Pausable', function (accounts) {
});
});
describe('exists', function () {
it('returns token existence', async function () {
expect(await this.token.$_exists(firstTokenId)).to.equal(true);
});
});
describe('isApprovedForAll', function () {
it('returns the approval of the operator', async function () {
expect(await this.token.isApprovedForAll(owner, operator)).to.equal(false);

View File

@ -86,7 +86,6 @@ contract('ERC721URIStorage', function (accounts) {
it('tokens without URI can be burnt ', async function () {
await this.token.$_burn(firstTokenId, { from: owner });
expect(await this.token.$_exists(firstTokenId)).to.equal(false);
await expectRevertCustomError(this.token.tokenURI(firstTokenId), 'ERC721NonexistentToken', [firstTokenId]);
});
@ -95,7 +94,6 @@ contract('ERC721URIStorage', function (accounts) {
await this.token.$_burn(firstTokenId, { from: owner });
expect(await this.token.$_exists(firstTokenId)).to.equal(false);
await expectRevertCustomError(this.token.tokenURI(firstTokenId), 'ERC721NonexistentToken', [firstTokenId]);
});
});