mirror of openzeppelin-contracts
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
openzeppelin-contracts/test/token/ERC1155/ERC1155.behavior.js

763 lines
28 KiB

const { ethers } = require('hardhat');
const { expect } = require('chai');
const { anyValue } = require('@nomicfoundation/hardhat-chai-matchers/withArgs');
const { RevertType } = require('../../helpers/enums');
const { shouldSupportInterfaces } = require('../../utils/introspection/SupportsInterface.behavior');
function shouldBehaveLikeERC1155() {
const firstTokenId = 1n;
const secondTokenId = 2n;
const unknownTokenId = 3n;
const firstTokenValue = 1000n;
const secondTokenValue = 2000n;
const RECEIVER_SINGLE_MAGIC_VALUE = '0xf23a6e61';
const RECEIVER_BATCH_MAGIC_VALUE = '0xbc197c81';
beforeEach(async function () {
[this.recipient, this.proxy, this.alice, this.bruce] = this.otherAccounts;
});
describe('like an ERC1155', function () {
describe('balanceOf', function () {
it('should return 0 when queried about the zero address', async function () {
expect(await this.token.balanceOf(ethers.ZeroAddress, firstTokenId)).to.equal(0n);
});
describe("when accounts don't own tokens", function () {
it('returns zero for given addresses', async function () {
expect(await this.token.balanceOf(this.alice, firstTokenId)).to.equal(0n);
expect(await this.token.balanceOf(this.bruce, secondTokenId)).to.equal(0n);
expect(await this.token.balanceOf(this.alice, unknownTokenId)).to.equal(0n);
});
});
describe('when accounts own some tokens', function () {
beforeEach(async function () {
await this.token.$_mint(this.alice, firstTokenId, firstTokenValue, '0x');
await this.token.$_mint(this.bruce, secondTokenId, secondTokenValue, '0x');
});
it('returns the amount of tokens owned by the given addresses', async function () {
expect(await this.token.balanceOf(this.alice, firstTokenId)).to.equal(firstTokenValue);
expect(await this.token.balanceOf(this.bruce, secondTokenId)).to.equal(secondTokenValue);
expect(await this.token.balanceOf(this.alice, unknownTokenId)).to.equal(0n);
});
});
});
describe('balanceOfBatch', function () {
it("reverts when input arrays don't match up", async function () {
const accounts1 = [this.alice, this.bruce, this.alice, this.bruce];
const ids1 = [firstTokenId, secondTokenId, unknownTokenId];
await expect(this.token.balanceOfBatch(accounts1, ids1))
.to.be.revertedWithCustomError(this.token, 'ERC1155InvalidArrayLength')
.withArgs(ids1.length, accounts1.length);
const accounts2 = [this.alice, this.bruce];
const ids2 = [firstTokenId, secondTokenId, unknownTokenId];
await expect(this.token.balanceOfBatch(accounts2, ids2))
.to.be.revertedWithCustomError(this.token, 'ERC1155InvalidArrayLength')
.withArgs(ids2.length, accounts2.length);
});
it('should return 0 as the balance when one of the addresses is the zero address', async function () {
const result = await this.token.balanceOfBatch(
[this.alice, this.bruce, ethers.ZeroAddress],
[firstTokenId, secondTokenId, unknownTokenId],
);
expect(result).to.deep.equal([0n, 0n, 0n]);
});
describe("when accounts don't own tokens", function () {
it('returns zeros for each account', async function () {
const result = await this.token.balanceOfBatch(
[this.alice, this.bruce, this.alice],
[firstTokenId, secondTokenId, unknownTokenId],
);
expect(result).to.deep.equal([0n, 0n, 0n]);
});
});
describe('when accounts own some tokens', function () {
beforeEach(async function () {
await this.token.$_mint(this.alice, firstTokenId, firstTokenValue, '0x');
await this.token.$_mint(this.bruce, secondTokenId, secondTokenValue, '0x');
});
it('returns amounts owned by each account in order passed', async function () {
const result = await this.token.balanceOfBatch(
[this.bruce, this.alice, this.alice],
[secondTokenId, firstTokenId, unknownTokenId],
);
expect(result).to.deep.equal([secondTokenValue, firstTokenValue, 0n]);
});
it('returns multiple times the balance of the same address when asked', async function () {
const result = await this.token.balanceOfBatch(
[this.alice, this.bruce, this.alice],
[firstTokenId, secondTokenId, firstTokenId],
);
expect(result).to.deep.equal([firstTokenValue, secondTokenValue, firstTokenValue]);
});
});
});
describe('setApprovalForAll', function () {
beforeEach(async function () {
this.tx = await this.token.connect(this.holder).setApprovalForAll(this.proxy, true);
});
it('sets approval status which can be queried via isApprovedForAll', async function () {
expect(await this.token.isApprovedForAll(this.holder, this.proxy)).to.be.true;
});
it('emits an ApprovalForAll log', async function () {
await expect(this.tx).to.emit(this.token, 'ApprovalForAll').withArgs(this.holder, this.proxy, true);
});
it('can unset approval for an operator', async function () {
await this.token.connect(this.holder).setApprovalForAll(this.proxy, false);
expect(await this.token.isApprovedForAll(this.holder, this.proxy)).to.be.false;
});
it('reverts if attempting to approve zero address as an operator', async function () {
await expect(this.token.connect(this.holder).setApprovalForAll(ethers.ZeroAddress, true))
.to.be.revertedWithCustomError(this.token, 'ERC1155InvalidOperator')
.withArgs(ethers.ZeroAddress);
});
});
describe('safeTransferFrom', function () {
beforeEach(async function () {
await this.token.$_mint(this.holder, firstTokenId, firstTokenValue, '0x');
await this.token.$_mint(this.holder, secondTokenId, secondTokenValue, '0x');
});
it('reverts when transferring more than balance', async function () {
await expect(
this.token
.connect(this.holder)
.safeTransferFrom(this.holder, this.recipient, firstTokenId, firstTokenValue + 1n, '0x'),
)
.to.be.revertedWithCustomError(this.token, 'ERC1155InsufficientBalance')
.withArgs(this.holder, firstTokenValue, firstTokenValue + 1n, firstTokenId);
});
it('reverts when transferring to zero address', async function () {
await expect(
this.token
.connect(this.holder)
.safeTransferFrom(this.holder, ethers.ZeroAddress, firstTokenId, firstTokenValue, '0x'),
)
.to.be.revertedWithCustomError(this.token, 'ERC1155InvalidReceiver')
.withArgs(ethers.ZeroAddress);
});
function transferWasSuccessful() {
it('debits transferred balance from sender', async function () {
expect(await this.token.balanceOf(this.args.from, this.args.id)).to.equal(0n);
});
it('credits transferred balance to receiver', async function () {
expect(await this.token.balanceOf(this.args.to, this.args.id)).to.equal(this.args.value);
});
it('emits a TransferSingle log', async function () {
await expect(this.tx)
.to.emit(this.token, 'TransferSingle')
.withArgs(this.args.operator, this.args.from, this.args.to, this.args.id, this.args.value);
});
}
describe('when called by the holder', async function () {
beforeEach(async function () {
this.args = {
operator: this.holder,
from: this.holder,
to: this.recipient,
id: firstTokenId,
value: firstTokenValue,
data: '0x',
};
this.tx = await this.token
.connect(this.args.operator)
.safeTransferFrom(this.args.from, this.args.to, this.args.id, this.args.value, this.args.data);
});
transferWasSuccessful();
it('preserves existing balances which are not transferred by holder', async function () {
expect(await this.token.balanceOf(this.holder, secondTokenId)).to.equal(secondTokenValue);
expect(await this.token.balanceOf(this.recipient, secondTokenId)).to.equal(0n);
});
});
describe('when called by an operator on behalf of the holder', function () {
describe('when operator is not approved by holder', function () {
beforeEach(async function () {
await this.token.connect(this.holder).setApprovalForAll(this.proxy, false);
});
it('reverts', async function () {
await expect(
this.token
.connect(this.proxy)
.safeTransferFrom(this.holder, this.recipient, firstTokenId, firstTokenValue, '0x'),
)
.to.be.revertedWithCustomError(this.token, 'ERC1155MissingApprovalForAll')
.withArgs(this.proxy, this.holder);
});
});
describe('when operator is approved by holder', function () {
beforeEach(async function () {
await this.token.connect(this.holder).setApprovalForAll(this.proxy, true);
this.args = {
operator: this.proxy,
from: this.holder,
to: this.recipient,
id: firstTokenId,
value: firstTokenValue,
data: '0x',
};
this.tx = await this.token
.connect(this.args.operator)
.safeTransferFrom(this.args.from, this.args.to, this.args.id, this.args.value, this.args.data);
});
transferWasSuccessful();
it("preserves operator's balances not involved in the transfer", async function () {
expect(await this.token.balanceOf(this.proxy, firstTokenId)).to.equal(0n);
expect(await this.token.balanceOf(this.proxy, secondTokenId)).to.equal(0n);
});
});
});
describe('when sending to a valid receiver', function () {
beforeEach(async function () {
this.receiver = await ethers.deployContract('$ERC1155ReceiverMock', [
RECEIVER_SINGLE_MAGIC_VALUE,
RECEIVER_BATCH_MAGIC_VALUE,
RevertType.None,
]);
});
describe('without data', function () {
beforeEach(async function () {
this.args = {
operator: this.holder,
from: this.holder,
to: this.receiver,
id: firstTokenId,
value: firstTokenValue,
data: '0x',
};
this.tx = await this.token
.connect(this.args.operator)
.safeTransferFrom(this.args.from, this.args.to, this.args.id, this.args.value, this.args.data);
});
transferWasSuccessful();
it('calls onERC1155Received', async function () {
await expect(this.tx)
.to.emit(this.receiver, 'Received')
.withArgs(this.args.operator, this.args.from, this.args.id, this.args.value, this.args.data, anyValue);
});
});
describe('with data', function () {
beforeEach(async function () {
this.args = {
operator: this.holder,
from: this.holder,
to: this.receiver,
id: firstTokenId,
value: firstTokenValue,
data: '0xf00dd00d',
};
this.tx = await this.token
.connect(this.args.operator)
.safeTransferFrom(this.args.from, this.args.to, this.args.id, this.args.value, this.args.data);
});
transferWasSuccessful();
it('calls onERC1155Received', async function () {
await expect(this.tx)
.to.emit(this.receiver, 'Received')
.withArgs(this.args.operator, this.args.from, this.args.id, this.args.value, this.args.data, anyValue);
});
});
});
describe('to a receiver contract returning unexpected value', function () {
it('reverts', async function () {
const receiver = await ethers.deployContract('$ERC1155ReceiverMock', [
'0x00c0ffee',
RECEIVER_BATCH_MAGIC_VALUE,
RevertType.None,
]);
await expect(
this.token
.connect(this.holder)
.safeTransferFrom(this.holder, receiver, firstTokenId, firstTokenValue, '0x'),
)
.to.be.revertedWithCustomError(this.token, 'ERC1155InvalidReceiver')
.withArgs(receiver);
});
});
describe('to a receiver contract that reverts', function () {
describe('with a revert string', function () {
it('reverts', async function () {
const receiver = await ethers.deployContract('$ERC1155ReceiverMock', [
RECEIVER_SINGLE_MAGIC_VALUE,
RECEIVER_BATCH_MAGIC_VALUE,
RevertType.RevertWithMessage,
]);
await expect(
this.token
.connect(this.holder)
.safeTransferFrom(this.holder, receiver, firstTokenId, firstTokenValue, '0x'),
).to.be.revertedWith('ERC1155ReceiverMock: reverting on receive');
});
});
describe('without a revert string', function () {
it('reverts', async function () {
const receiver = await ethers.deployContract('$ERC1155ReceiverMock', [
RECEIVER_SINGLE_MAGIC_VALUE,
RECEIVER_BATCH_MAGIC_VALUE,
RevertType.RevertWithoutMessage,
]);
await expect(
this.token
.connect(this.holder)
.safeTransferFrom(this.holder, receiver, firstTokenId, firstTokenValue, '0x'),
)
.to.be.revertedWithCustomError(this.token, 'ERC1155InvalidReceiver')
.withArgs(receiver);
});
});
describe('with a custom error', function () {
it('reverts', async function () {
const receiver = await ethers.deployContract('$ERC1155ReceiverMock', [
RECEIVER_SINGLE_MAGIC_VALUE,
RECEIVER_BATCH_MAGIC_VALUE,
RevertType.RevertWithCustomError,
]);
await expect(
this.token
.connect(this.holder)
.safeTransferFrom(this.holder, receiver, firstTokenId, firstTokenValue, '0x'),
)
.to.be.revertedWithCustomError(receiver, 'CustomError')
.withArgs(RECEIVER_SINGLE_MAGIC_VALUE);
});
});
describe('with a panic', function () {
it('reverts', async function () {
const receiver = await ethers.deployContract('$ERC1155ReceiverMock', [
RECEIVER_SINGLE_MAGIC_VALUE,
RECEIVER_BATCH_MAGIC_VALUE,
RevertType.Panic,
]);
await expect(
this.token
.connect(this.holder)
.safeTransferFrom(this.holder, receiver, firstTokenId, firstTokenValue, '0x'),
).to.be.revertedWithPanic();
});
});
});
describe('to a contract that does not implement the required function', function () {
it('reverts', async function () {
const invalidReceiver = this.token;
await expect(
this.token
.connect(this.holder)
.safeTransferFrom(this.holder, invalidReceiver, firstTokenId, firstTokenValue, '0x'),
)
.to.be.revertedWithCustomError(this.token, 'ERC1155InvalidReceiver')
.withArgs(invalidReceiver);
});
});
});
describe('safeBatchTransferFrom', function () {
beforeEach(async function () {
await this.token.$_mint(this.holder, firstTokenId, firstTokenValue, '0x');
await this.token.$_mint(this.holder, secondTokenId, secondTokenValue, '0x');
});
it('reverts when transferring value more than any of balances', async function () {
await expect(
this.token
.connect(this.holder)
.safeBatchTransferFrom(
this.holder,
this.recipient,
[firstTokenId, secondTokenId],
[firstTokenValue, secondTokenValue + 1n],
'0x',
),
)
.to.be.revertedWithCustomError(this.token, 'ERC1155InsufficientBalance')
.withArgs(this.holder, secondTokenValue, secondTokenValue + 1n, secondTokenId);
});
it("reverts when ids array length doesn't match values array length", async function () {
const ids1 = [firstTokenId];
const tokenValues1 = [firstTokenValue, secondTokenValue];
await expect(
this.token.connect(this.holder).safeBatchTransferFrom(this.holder, this.recipient, ids1, tokenValues1, '0x'),
)
.to.be.revertedWithCustomError(this.token, 'ERC1155InvalidArrayLength')
.withArgs(ids1.length, tokenValues1.length);
const ids2 = [firstTokenId, secondTokenId];
const tokenValues2 = [firstTokenValue];
await expect(
this.token.connect(this.holder).safeBatchTransferFrom(this.holder, this.recipient, ids2, tokenValues2, '0x'),
)
.to.be.revertedWithCustomError(this.token, 'ERC1155InvalidArrayLength')
.withArgs(ids2.length, tokenValues2.length);
});
it('reverts when transferring to zero address', async function () {
await expect(
this.token
.connect(this.holder)
.safeBatchTransferFrom(
this.holder,
ethers.ZeroAddress,
[firstTokenId, secondTokenId],
[firstTokenValue, secondTokenValue],
'0x',
),
)
.to.be.revertedWithCustomError(this.token, 'ERC1155InvalidReceiver')
.withArgs(ethers.ZeroAddress);
});
it('reverts when transferring from zero address', async function () {
await expect(
this.token.$_safeBatchTransferFrom(ethers.ZeroAddress, this.holder, [firstTokenId], [firstTokenValue], '0x'),
)
.to.be.revertedWithCustomError(this.token, 'ERC1155InvalidSender')
.withArgs(ethers.ZeroAddress);
});
function batchTransferWasSuccessful() {
it('debits transferred balances from sender', async function () {
const newBalances = await this.token.balanceOfBatch(
this.args.ids.map(() => this.args.from),
this.args.ids,
);
expect(newBalances).to.deep.equal(this.args.ids.map(() => 0n));
});
it('credits transferred balances to receiver', async function () {
const newBalances = await this.token.balanceOfBatch(
this.args.ids.map(() => this.args.to),
this.args.ids,
);
expect(newBalances).to.deep.equal(this.args.values);
});
it('emits a TransferBatch log', async function () {
await expect(this.tx)
.to.emit(this.token, 'TransferBatch')
.withArgs(this.args.operator, this.args.from, this.args.to, this.args.ids, this.args.values);
});
}
describe('when called by the holder', async function () {
beforeEach(async function () {
this.args = {
operator: this.holder,
from: this.holder,
to: this.recipient,
ids: [firstTokenId, secondTokenId],
values: [firstTokenValue, secondTokenValue],
data: '0x',
};
this.tx = await this.token
.connect(this.args.operator)
.safeBatchTransferFrom(this.args.from, this.args.to, this.args.ids, this.args.values, this.args.data);
});
batchTransferWasSuccessful();
});
describe('when called by an operator on behalf of the holder', function () {
describe('when operator is not approved by holder', function () {
beforeEach(async function () {
await this.token.connect(this.holder).setApprovalForAll(this.proxy, false);
});
it('reverts', async function () {
await expect(
this.token
.connect(this.proxy)
.safeBatchTransferFrom(
this.holder,
this.recipient,
[firstTokenId, secondTokenId],
[firstTokenValue, secondTokenValue],
'0x',
),
)
.to.be.revertedWithCustomError(this.token, 'ERC1155MissingApprovalForAll')
.withArgs(this.proxy, this.holder);
});
});
describe('when operator is approved by holder', function () {
beforeEach(async function () {
await this.token.connect(this.holder).setApprovalForAll(this.proxy, true);
this.args = {
operator: this.proxy,
from: this.holder,
to: this.recipient,
ids: [firstTokenId, secondTokenId],
values: [firstTokenValue, secondTokenValue],
data: '0x',
};
this.tx = await this.token
.connect(this.args.operator)
.safeBatchTransferFrom(this.args.from, this.args.to, this.args.ids, this.args.values, this.args.data);
});
batchTransferWasSuccessful();
it("preserves operator's balances not involved in the transfer", async function () {
expect(await this.token.balanceOf(this.proxy, firstTokenId)).to.equal(0n);
expect(await this.token.balanceOf(this.proxy, secondTokenId)).to.equal(0n);
});
});
});
describe('when sending to a valid receiver', function () {
beforeEach(async function () {
this.receiver = await ethers.deployContract('$ERC1155ReceiverMock', [
RECEIVER_SINGLE_MAGIC_VALUE,
RECEIVER_BATCH_MAGIC_VALUE,
RevertType.None,
]);
});
describe('without data', function () {
beforeEach(async function () {
this.args = {
operator: this.holder,
from: this.holder,
to: this.receiver,
ids: [firstTokenId, secondTokenId],
values: [firstTokenValue, secondTokenValue],
data: '0x',
};
this.tx = await this.token
.connect(this.args.operator)
.safeBatchTransferFrom(this.args.from, this.args.to, this.args.ids, this.args.values, this.args.data);
});
batchTransferWasSuccessful();
it('calls onERC1155BatchReceived', async function () {
await expect(this.tx)
.to.emit(this.receiver, 'BatchReceived')
.withArgs(this.holder, this.holder, this.args.ids, this.args.values, this.args.data, anyValue);
});
});
describe('with data', function () {
beforeEach(async function () {
this.args = {
operator: this.holder,
from: this.holder,
to: this.receiver,
ids: [firstTokenId, secondTokenId],
values: [firstTokenValue, secondTokenValue],
data: '0xf00dd00d',
};
this.tx = await this.token
.connect(this.args.operator)
.safeBatchTransferFrom(this.args.from, this.args.to, this.args.ids, this.args.values, this.args.data);
});
batchTransferWasSuccessful();
it('calls onERC1155Received', async function () {
await expect(this.tx)
.to.emit(this.receiver, 'BatchReceived')
.withArgs(this.holder, this.holder, this.args.ids, this.args.values, this.args.data, anyValue);
});
});
});
describe('to a receiver contract returning unexpected value', function () {
it('reverts', async function () {
const receiver = await ethers.deployContract('$ERC1155ReceiverMock', [
RECEIVER_SINGLE_MAGIC_VALUE,
RECEIVER_SINGLE_MAGIC_VALUE,
RevertType.None,
]);
await expect(
this.token
.connect(this.holder)
.safeBatchTransferFrom(
this.holder,
receiver,
[firstTokenId, secondTokenId],
[firstTokenValue, secondTokenValue],
'0x',
),
)
.to.be.revertedWithCustomError(this.token, 'ERC1155InvalidReceiver')
.withArgs(receiver);
});
});
describe('to a receiver contract that reverts', function () {
describe('with a revert string', function () {
it('reverts', async function () {
const receiver = await ethers.deployContract('$ERC1155ReceiverMock', [
RECEIVER_SINGLE_MAGIC_VALUE,
RECEIVER_BATCH_MAGIC_VALUE,
RevertType.RevertWithMessage,
]);
await expect(
this.token
.connect(this.holder)
.safeBatchTransferFrom(
this.holder,
receiver,
[firstTokenId, secondTokenId],
[firstTokenValue, secondTokenValue],
'0x',
),
).to.be.revertedWith('ERC1155ReceiverMock: reverting on batch receive');
});
});
describe('without a revert string', function () {
it('reverts', async function () {
const receiver = await ethers.deployContract('$ERC1155ReceiverMock', [
RECEIVER_SINGLE_MAGIC_VALUE,
RECEIVER_BATCH_MAGIC_VALUE,
RevertType.RevertWithoutMessage,
]);
await expect(
this.token
.connect(this.holder)
.safeBatchTransferFrom(
this.holder,
receiver,
[firstTokenId, secondTokenId],
[firstTokenValue, secondTokenValue],
'0x',
),
)
.to.be.revertedWithCustomError(this.token, 'ERC1155InvalidReceiver')
.withArgs(receiver);
});
});
describe('with a custom error', function () {
it('reverts', async function () {
const receiver = await ethers.deployContract('$ERC1155ReceiverMock', [
RECEIVER_SINGLE_MAGIC_VALUE,
RECEIVER_BATCH_MAGIC_VALUE,
RevertType.RevertWithCustomError,
]);
await expect(
this.token
.connect(this.holder)
.safeBatchTransferFrom(
this.holder,
receiver,
[firstTokenId, secondTokenId],
[firstTokenValue, secondTokenValue],
'0x',
),
)
.to.be.revertedWithCustomError(receiver, 'CustomError')
.withArgs(RECEIVER_SINGLE_MAGIC_VALUE);
});
});
describe('with a panic', function () {
it('reverts', async function () {
const receiver = await ethers.deployContract('$ERC1155ReceiverMock', [
RECEIVER_SINGLE_MAGIC_VALUE,
RECEIVER_BATCH_MAGIC_VALUE,
RevertType.Panic,
]);
await expect(
this.token
.connect(this.holder)
.safeBatchTransferFrom(
this.holder,
receiver,
[firstTokenId, secondTokenId],
[firstTokenValue, secondTokenValue],
'0x',
),
).to.be.revertedWithPanic();
});
});
});
describe('to a contract that does not implement the required function', function () {
it('reverts', async function () {
const invalidReceiver = this.token;
await expect(
this.token
.connect(this.holder)
.safeBatchTransferFrom(
this.holder,
invalidReceiver,
[firstTokenId, secondTokenId],
[firstTokenValue, secondTokenValue],
'0x',
),
)
.to.be.revertedWithCustomError(this.token, 'ERC1155InvalidReceiver')
.withArgs(invalidReceiver);
});
});
});
shouldSupportInterfaces(['ERC1155', 'ERC1155MetadataURI']);
});
}
module.exports = {
shouldBehaveLikeERC1155,
};