dexorder
This commit is contained in:
@@ -0,0 +1,763 @@
|
||||
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', 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', 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,
|
||||
};
|
||||
213
lib_openzeppelin_contracts/test/token/ERC1155/ERC1155.test.js
Normal file
213
lib_openzeppelin_contracts/test/token/ERC1155/ERC1155.test.js
Normal file
@@ -0,0 +1,213 @@
|
||||
const { ethers } = require('hardhat');
|
||||
const { expect } = require('chai');
|
||||
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
|
||||
|
||||
const { zip } = require('../../helpers/iterate');
|
||||
const { shouldBehaveLikeERC1155 } = require('./ERC1155.behavior');
|
||||
|
||||
const initialURI = 'https://token-cdn-domain/{id}.json';
|
||||
|
||||
async function fixture() {
|
||||
const [operator, holder, ...otherAccounts] = await ethers.getSigners();
|
||||
const token = await ethers.deployContract('$ERC1155', [initialURI]);
|
||||
return { token, operator, holder, otherAccounts };
|
||||
}
|
||||
|
||||
describe('ERC1155', function () {
|
||||
beforeEach(async function () {
|
||||
Object.assign(this, await loadFixture(fixture));
|
||||
});
|
||||
|
||||
shouldBehaveLikeERC1155();
|
||||
|
||||
describe('internal functions', function () {
|
||||
const tokenId = 1990n;
|
||||
const mintValue = 9001n;
|
||||
const burnValue = 3000n;
|
||||
|
||||
const tokenBatchIds = [2000n, 2010n, 2020n];
|
||||
const mintValues = [5000n, 10000n, 42195n];
|
||||
const burnValues = [5000n, 9001n, 195n];
|
||||
|
||||
const data = '0x12345678';
|
||||
|
||||
describe('_mint', function () {
|
||||
it('reverts with a zero destination address', async function () {
|
||||
await expect(this.token.$_mint(ethers.ZeroAddress, tokenId, mintValue, data))
|
||||
.to.be.revertedWithCustomError(this.token, 'ERC1155InvalidReceiver')
|
||||
.withArgs(ethers.ZeroAddress);
|
||||
});
|
||||
|
||||
describe('with minted tokens', function () {
|
||||
beforeEach(async function () {
|
||||
this.tx = await this.token.connect(this.operator).$_mint(this.holder, tokenId, mintValue, data);
|
||||
});
|
||||
|
||||
it('emits a TransferSingle event', async function () {
|
||||
await expect(this.tx)
|
||||
.to.emit(this.token, 'TransferSingle')
|
||||
.withArgs(this.operator, ethers.ZeroAddress, this.holder, tokenId, mintValue);
|
||||
});
|
||||
|
||||
it('credits the minted token value', async function () {
|
||||
expect(await this.token.balanceOf(this.holder, tokenId)).to.equal(mintValue);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('_mintBatch', function () {
|
||||
it('reverts with a zero destination address', async function () {
|
||||
await expect(this.token.$_mintBatch(ethers.ZeroAddress, tokenBatchIds, mintValues, data))
|
||||
.to.be.revertedWithCustomError(this.token, 'ERC1155InvalidReceiver')
|
||||
.withArgs(ethers.ZeroAddress);
|
||||
});
|
||||
|
||||
it('reverts if length of inputs do not match', async function () {
|
||||
await expect(this.token.$_mintBatch(this.holder, tokenBatchIds, mintValues.slice(1), data))
|
||||
.to.be.revertedWithCustomError(this.token, 'ERC1155InvalidArrayLength')
|
||||
.withArgs(tokenBatchIds.length, mintValues.length - 1);
|
||||
|
||||
await expect(this.token.$_mintBatch(this.holder, tokenBatchIds.slice(1), mintValues, data))
|
||||
.to.be.revertedWithCustomError(this.token, 'ERC1155InvalidArrayLength')
|
||||
.withArgs(tokenBatchIds.length - 1, mintValues.length);
|
||||
});
|
||||
|
||||
describe('with minted batch of tokens', function () {
|
||||
beforeEach(async function () {
|
||||
this.tx = await this.token.connect(this.operator).$_mintBatch(this.holder, tokenBatchIds, mintValues, data);
|
||||
});
|
||||
|
||||
it('emits a TransferBatch event', async function () {
|
||||
await expect(this.tx)
|
||||
.to.emit(this.token, 'TransferBatch')
|
||||
.withArgs(this.operator, ethers.ZeroAddress, this.holder, tokenBatchIds, mintValues);
|
||||
});
|
||||
|
||||
it('credits the minted batch of tokens', async function () {
|
||||
const holderBatchBalances = await this.token.balanceOfBatch(
|
||||
tokenBatchIds.map(() => this.holder),
|
||||
tokenBatchIds,
|
||||
);
|
||||
|
||||
expect(holderBatchBalances).to.deep.equal(mintValues);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('_burn', function () {
|
||||
it("reverts when burning the zero account's tokens", async function () {
|
||||
await expect(this.token.$_burn(ethers.ZeroAddress, tokenId, mintValue))
|
||||
.to.be.revertedWithCustomError(this.token, 'ERC1155InvalidSender')
|
||||
.withArgs(ethers.ZeroAddress);
|
||||
});
|
||||
|
||||
it('reverts when burning a non-existent token id', async function () {
|
||||
await expect(this.token.$_burn(this.holder, tokenId, mintValue))
|
||||
.to.be.revertedWithCustomError(this.token, 'ERC1155InsufficientBalance')
|
||||
.withArgs(this.holder, 0, mintValue, tokenId);
|
||||
});
|
||||
|
||||
it('reverts when burning more than available tokens', async function () {
|
||||
await this.token.connect(this.operator).$_mint(this.holder, tokenId, mintValue, data);
|
||||
|
||||
await expect(this.token.$_burn(this.holder, tokenId, mintValue + 1n))
|
||||
.to.be.revertedWithCustomError(this.token, 'ERC1155InsufficientBalance')
|
||||
.withArgs(this.holder, mintValue, mintValue + 1n, tokenId);
|
||||
});
|
||||
|
||||
describe('with minted-then-burnt tokens', function () {
|
||||
beforeEach(async function () {
|
||||
await this.token.$_mint(this.holder, tokenId, mintValue, data);
|
||||
this.tx = await this.token.connect(this.operator).$_burn(this.holder, tokenId, burnValue);
|
||||
});
|
||||
|
||||
it('emits a TransferSingle event', async function () {
|
||||
await expect(this.tx)
|
||||
.to.emit(this.token, 'TransferSingle')
|
||||
.withArgs(this.operator, this.holder, ethers.ZeroAddress, tokenId, burnValue);
|
||||
});
|
||||
|
||||
it('accounts for both minting and burning', async function () {
|
||||
expect(await this.token.balanceOf(this.holder, tokenId)).to.equal(mintValue - burnValue);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('_burnBatch', function () {
|
||||
it("reverts when burning the zero account's tokens", async function () {
|
||||
await expect(this.token.$_burnBatch(ethers.ZeroAddress, tokenBatchIds, burnValues))
|
||||
.to.be.revertedWithCustomError(this.token, 'ERC1155InvalidSender')
|
||||
.withArgs(ethers.ZeroAddress);
|
||||
});
|
||||
|
||||
it('reverts if length of inputs do not match', async function () {
|
||||
await expect(this.token.$_burnBatch(this.holder, tokenBatchIds, burnValues.slice(1)))
|
||||
.to.be.revertedWithCustomError(this.token, 'ERC1155InvalidArrayLength')
|
||||
.withArgs(tokenBatchIds.length, burnValues.length - 1);
|
||||
|
||||
await expect(this.token.$_burnBatch(this.holder, tokenBatchIds.slice(1), burnValues))
|
||||
.to.be.revertedWithCustomError(this.token, 'ERC1155InvalidArrayLength')
|
||||
.withArgs(tokenBatchIds.length - 1, burnValues.length);
|
||||
});
|
||||
|
||||
it('reverts when burning a non-existent token id', async function () {
|
||||
await expect(this.token.$_burnBatch(this.holder, tokenBatchIds, burnValues))
|
||||
.to.be.revertedWithCustomError(this.token, 'ERC1155InsufficientBalance')
|
||||
.withArgs(this.holder, 0, burnValues[0], tokenBatchIds[0]);
|
||||
});
|
||||
|
||||
describe('with minted-then-burnt tokens', function () {
|
||||
beforeEach(async function () {
|
||||
await this.token.$_mintBatch(this.holder, tokenBatchIds, mintValues, data);
|
||||
this.tx = await this.token.connect(this.operator).$_burnBatch(this.holder, tokenBatchIds, burnValues);
|
||||
});
|
||||
|
||||
it('emits a TransferBatch event', async function () {
|
||||
await expect(this.tx)
|
||||
.to.emit(this.token, 'TransferBatch')
|
||||
.withArgs(this.operator, this.holder, ethers.ZeroAddress, tokenBatchIds, burnValues);
|
||||
});
|
||||
|
||||
it('accounts for both minting and burning', async function () {
|
||||
const holderBatchBalances = await this.token.balanceOfBatch(
|
||||
tokenBatchIds.map(() => this.holder),
|
||||
tokenBatchIds,
|
||||
);
|
||||
|
||||
expect(holderBatchBalances).to.deep.equal(
|
||||
zip(mintValues, burnValues).map(([mintValue, burnValue]) => mintValue - burnValue),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('ERC1155MetadataURI', function () {
|
||||
const firstTokenID = 42n;
|
||||
const secondTokenID = 1337n;
|
||||
|
||||
it('emits no URI event in constructor', async function () {
|
||||
await expect(this.token.deploymentTransaction()).to.not.emit(this.token, 'URI');
|
||||
});
|
||||
|
||||
it('sets the initial URI for all token types', async function () {
|
||||
expect(await this.token.uri(firstTokenID)).to.equal(initialURI);
|
||||
expect(await this.token.uri(secondTokenID)).to.equal(initialURI);
|
||||
});
|
||||
|
||||
describe('_setURI', function () {
|
||||
const newURI = 'https://token-cdn-domain/{locale}/{id}.json';
|
||||
|
||||
it('emits no URI event', async function () {
|
||||
await expect(this.token.$_setURI(newURI)).to.not.emit(this.token, 'URI');
|
||||
});
|
||||
|
||||
it('sets the new URI for all token types', async function () {
|
||||
await this.token.$_setURI(newURI);
|
||||
|
||||
expect(await this.token.uri(firstTokenID)).to.equal(newURI);
|
||||
expect(await this.token.uri(secondTokenID)).to.equal(newURI);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,66 @@
|
||||
const { ethers } = require('hardhat');
|
||||
const { expect } = require('chai');
|
||||
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
|
||||
|
||||
const ids = [42n, 1137n];
|
||||
const values = [3000n, 9902n];
|
||||
|
||||
async function fixture() {
|
||||
const [holder, operator, other] = await ethers.getSigners();
|
||||
|
||||
const token = await ethers.deployContract('$ERC1155Burnable', ['https://token-cdn-domain/{id}.json']);
|
||||
await token.$_mint(holder, ids[0], values[0], '0x');
|
||||
await token.$_mint(holder, ids[1], values[1], '0x');
|
||||
|
||||
return { token, holder, operator, other };
|
||||
}
|
||||
|
||||
describe('ERC1155Burnable', function () {
|
||||
beforeEach(async function () {
|
||||
Object.assign(this, await loadFixture(fixture));
|
||||
});
|
||||
|
||||
describe('burn', function () {
|
||||
it('holder can burn their tokens', async function () {
|
||||
await this.token.connect(this.holder).burn(this.holder, ids[0], values[0] - 1n);
|
||||
|
||||
expect(await this.token.balanceOf(this.holder, ids[0])).to.equal(1n);
|
||||
});
|
||||
|
||||
it("approved operators can burn the holder's tokens", async function () {
|
||||
await this.token.connect(this.holder).setApprovalForAll(this.operator, true);
|
||||
await this.token.connect(this.operator).burn(this.holder, ids[0], values[0] - 1n);
|
||||
|
||||
expect(await this.token.balanceOf(this.holder, ids[0])).to.equal(1n);
|
||||
});
|
||||
|
||||
it("unapproved accounts cannot burn the holder's tokens", async function () {
|
||||
await expect(this.token.connect(this.other).burn(this.holder, ids[0], values[0] - 1n))
|
||||
.to.be.revertedWithCustomError(this.token, 'ERC1155MissingApprovalForAll')
|
||||
.withArgs(this.other, this.holder);
|
||||
});
|
||||
});
|
||||
|
||||
describe('burnBatch', function () {
|
||||
it('holder can burn their tokens', async function () {
|
||||
await this.token.connect(this.holder).burnBatch(this.holder, ids, [values[0] - 1n, values[1] - 2n]);
|
||||
|
||||
expect(await this.token.balanceOf(this.holder, ids[0])).to.equal(1n);
|
||||
expect(await this.token.balanceOf(this.holder, ids[1])).to.equal(2n);
|
||||
});
|
||||
|
||||
it("approved operators can burn the holder's tokens", async function () {
|
||||
await this.token.connect(this.holder).setApprovalForAll(this.operator, true);
|
||||
await this.token.connect(this.operator).burnBatch(this.holder, ids, [values[0] - 1n, values[1] - 2n]);
|
||||
|
||||
expect(await this.token.balanceOf(this.holder, ids[0])).to.equal(1n);
|
||||
expect(await this.token.balanceOf(this.holder, ids[1])).to.equal(2n);
|
||||
});
|
||||
|
||||
it("unapproved accounts cannot burn the holder's tokens", async function () {
|
||||
await expect(this.token.connect(this.other).burnBatch(this.holder, ids, [values[0] - 1n, values[1] - 2n]))
|
||||
.to.be.revertedWithCustomError(this.token, 'ERC1155MissingApprovalForAll')
|
||||
.withArgs(this.other, this.holder);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,105 @@
|
||||
const { ethers } = require('hardhat');
|
||||
const { expect } = require('chai');
|
||||
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
|
||||
|
||||
async function fixture() {
|
||||
const [holder, operator, receiver, other] = await ethers.getSigners();
|
||||
const token = await ethers.deployContract('$ERC1155Pausable', ['https://token-cdn-domain/{id}.json']);
|
||||
return { token, holder, operator, receiver, other };
|
||||
}
|
||||
|
||||
describe('ERC1155Pausable', function () {
|
||||
const firstTokenId = 37n;
|
||||
const firstTokenValue = 42n;
|
||||
const secondTokenId = 19842n;
|
||||
const secondTokenValue = 23n;
|
||||
|
||||
beforeEach(async function () {
|
||||
Object.assign(this, await loadFixture(fixture));
|
||||
});
|
||||
|
||||
describe('when token is paused', function () {
|
||||
beforeEach(async function () {
|
||||
await this.token.connect(this.holder).setApprovalForAll(this.operator, true);
|
||||
await this.token.$_mint(this.holder, firstTokenId, firstTokenValue, '0x');
|
||||
await this.token.$_pause();
|
||||
});
|
||||
|
||||
it('reverts when trying to safeTransferFrom from holder', async function () {
|
||||
await expect(
|
||||
this.token
|
||||
.connect(this.holder)
|
||||
.safeTransferFrom(this.holder, this.receiver, firstTokenId, firstTokenValue, '0x'),
|
||||
).to.be.revertedWithCustomError(this.token, 'EnforcedPause');
|
||||
});
|
||||
|
||||
it('reverts when trying to safeTransferFrom from operator', async function () {
|
||||
await expect(
|
||||
this.token
|
||||
.connect(this.operator)
|
||||
.safeTransferFrom(this.holder, this.receiver, firstTokenId, firstTokenValue, '0x'),
|
||||
).to.be.revertedWithCustomError(this.token, 'EnforcedPause');
|
||||
});
|
||||
|
||||
it('reverts when trying to safeBatchTransferFrom from holder', async function () {
|
||||
await expect(
|
||||
this.token
|
||||
.connect(this.holder)
|
||||
.safeBatchTransferFrom(this.holder, this.receiver, [firstTokenId], [firstTokenValue], '0x'),
|
||||
).to.be.revertedWithCustomError(this.token, 'EnforcedPause');
|
||||
});
|
||||
|
||||
it('reverts when trying to safeBatchTransferFrom from operator', async function () {
|
||||
await expect(
|
||||
this.token
|
||||
.connect(this.operator)
|
||||
.safeBatchTransferFrom(this.holder, this.receiver, [firstTokenId], [firstTokenValue], '0x'),
|
||||
).to.be.revertedWithCustomError(this.token, 'EnforcedPause');
|
||||
});
|
||||
|
||||
it('reverts when trying to mint', async function () {
|
||||
await expect(this.token.$_mint(this.holder, secondTokenId, secondTokenValue, '0x')).to.be.revertedWithCustomError(
|
||||
this.token,
|
||||
'EnforcedPause',
|
||||
);
|
||||
});
|
||||
|
||||
it('reverts when trying to mintBatch', async function () {
|
||||
await expect(
|
||||
this.token.$_mintBatch(this.holder, [secondTokenId], [secondTokenValue], '0x'),
|
||||
).to.be.revertedWithCustomError(this.token, 'EnforcedPause');
|
||||
});
|
||||
|
||||
it('reverts when trying to burn', async function () {
|
||||
await expect(this.token.$_burn(this.holder, firstTokenId, firstTokenValue)).to.be.revertedWithCustomError(
|
||||
this.token,
|
||||
'EnforcedPause',
|
||||
);
|
||||
});
|
||||
|
||||
it('reverts when trying to burnBatch', async function () {
|
||||
await expect(
|
||||
this.token.$_burnBatch(this.holder, [firstTokenId], [firstTokenValue]),
|
||||
).to.be.revertedWithCustomError(this.token, 'EnforcedPause');
|
||||
});
|
||||
|
||||
describe('setApprovalForAll', function () {
|
||||
it('approves an operator', async function () {
|
||||
await this.token.connect(this.holder).setApprovalForAll(this.other, true);
|
||||
expect(await this.token.isApprovedForAll(this.holder, this.other)).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
describe('balanceOf', function () {
|
||||
it('returns the token value owned by the given address', async function () {
|
||||
expect(await this.token.balanceOf(this.holder, firstTokenId)).to.equal(firstTokenValue);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isApprovedForAll', function () {
|
||||
it('returns the approval of the operator', async function () {
|
||||
expect(await this.token.isApprovedForAll(this.holder, this.operator)).to.be.true;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,119 @@
|
||||
const { ethers } = require('hardhat');
|
||||
const { expect } = require('chai');
|
||||
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
|
||||
|
||||
async function fixture() {
|
||||
const [holder] = await ethers.getSigners();
|
||||
const token = await ethers.deployContract('$ERC1155Supply', ['https://token-cdn-domain/{id}.json']);
|
||||
return { token, holder };
|
||||
}
|
||||
|
||||
describe('ERC1155Supply', function () {
|
||||
const firstTokenId = 37n;
|
||||
const firstTokenValue = 42n;
|
||||
const secondTokenId = 19842n;
|
||||
const secondTokenValue = 23n;
|
||||
|
||||
beforeEach(async function () {
|
||||
Object.assign(this, await loadFixture(fixture));
|
||||
});
|
||||
|
||||
describe('before mint', function () {
|
||||
it('exist', async function () {
|
||||
expect(await this.token.exists(firstTokenId)).to.be.false;
|
||||
});
|
||||
|
||||
it('totalSupply', async function () {
|
||||
expect(await this.token.totalSupply(ethers.Typed.uint256(firstTokenId))).to.equal(0n);
|
||||
expect(await this.token.totalSupply()).to.equal(0n);
|
||||
});
|
||||
});
|
||||
|
||||
describe('after mint', function () {
|
||||
describe('single', function () {
|
||||
beforeEach(async function () {
|
||||
await this.token.$_mint(this.holder, firstTokenId, firstTokenValue, '0x');
|
||||
});
|
||||
|
||||
it('exist', async function () {
|
||||
expect(await this.token.exists(firstTokenId)).to.be.true;
|
||||
});
|
||||
|
||||
it('totalSupply', async function () {
|
||||
expect(await this.token.totalSupply(ethers.Typed.uint256(firstTokenId))).to.equal(firstTokenValue);
|
||||
expect(await this.token.totalSupply()).to.equal(firstTokenValue);
|
||||
});
|
||||
});
|
||||
|
||||
describe('batch', function () {
|
||||
beforeEach(async function () {
|
||||
await this.token.$_mintBatch(
|
||||
this.holder,
|
||||
[firstTokenId, secondTokenId],
|
||||
[firstTokenValue, secondTokenValue],
|
||||
'0x',
|
||||
);
|
||||
});
|
||||
|
||||
it('exist', async function () {
|
||||
expect(await this.token.exists(firstTokenId)).to.be.true;
|
||||
expect(await this.token.exists(secondTokenId)).to.be.true;
|
||||
});
|
||||
|
||||
it('totalSupply', async function () {
|
||||
expect(await this.token.totalSupply(ethers.Typed.uint256(firstTokenId))).to.equal(firstTokenValue);
|
||||
expect(await this.token.totalSupply(ethers.Typed.uint256(secondTokenId))).to.equal(secondTokenValue);
|
||||
expect(await this.token.totalSupply()).to.equal(firstTokenValue + secondTokenValue);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('after burn', function () {
|
||||
describe('single', function () {
|
||||
beforeEach(async function () {
|
||||
await this.token.$_mint(this.holder, firstTokenId, firstTokenValue, '0x');
|
||||
await this.token.$_burn(this.holder, firstTokenId, firstTokenValue);
|
||||
});
|
||||
|
||||
it('exist', async function () {
|
||||
expect(await this.token.exists(firstTokenId)).to.be.false;
|
||||
});
|
||||
|
||||
it('totalSupply', async function () {
|
||||
expect(await this.token.totalSupply(ethers.Typed.uint256(firstTokenId))).to.equal(0n);
|
||||
expect(await this.token.totalSupply()).to.equal(0n);
|
||||
});
|
||||
});
|
||||
|
||||
describe('batch', function () {
|
||||
beforeEach(async function () {
|
||||
await this.token.$_mintBatch(
|
||||
this.holder,
|
||||
[firstTokenId, secondTokenId],
|
||||
[firstTokenValue, secondTokenValue],
|
||||
'0x',
|
||||
);
|
||||
await this.token.$_burnBatch(this.holder, [firstTokenId, secondTokenId], [firstTokenValue, secondTokenValue]);
|
||||
});
|
||||
|
||||
it('exist', async function () {
|
||||
expect(await this.token.exists(firstTokenId)).to.be.false;
|
||||
expect(await this.token.exists(secondTokenId)).to.be.false;
|
||||
});
|
||||
|
||||
it('totalSupply', async function () {
|
||||
expect(await this.token.totalSupply(ethers.Typed.uint256(firstTokenId))).to.equal(0n);
|
||||
expect(await this.token.totalSupply(ethers.Typed.uint256(secondTokenId))).to.equal(0n);
|
||||
expect(await this.token.totalSupply()).to.equal(0n);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('other', function () {
|
||||
it('supply unaffected by no-op', async function () {
|
||||
await this.token.$_update(ethers.ZeroAddress, ethers.ZeroAddress, [firstTokenId], [firstTokenValue]);
|
||||
expect(await this.token.totalSupply(ethers.Typed.uint256(firstTokenId))).to.equal(0n);
|
||||
expect(await this.token.totalSupply()).to.equal(0n);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,70 @@
|
||||
const { ethers } = require('hardhat');
|
||||
const { expect } = require('chai');
|
||||
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
|
||||
|
||||
const erc1155Uri = 'https://token.com/nfts/';
|
||||
const baseUri = 'https://token.com/';
|
||||
const tokenId = 1n;
|
||||
const value = 3000n;
|
||||
|
||||
describe('ERC1155URIStorage', function () {
|
||||
describe('with base uri set', function () {
|
||||
async function fixture() {
|
||||
const [holder] = await ethers.getSigners();
|
||||
|
||||
const token = await ethers.deployContract('$ERC1155URIStorage', [erc1155Uri]);
|
||||
await token.$_setBaseURI(baseUri);
|
||||
await token.$_mint(holder, tokenId, value, '0x');
|
||||
|
||||
return { token, holder };
|
||||
}
|
||||
|
||||
beforeEach(async function () {
|
||||
Object.assign(this, await loadFixture(fixture));
|
||||
});
|
||||
|
||||
it('can request the token uri, returning the erc1155 uri if no token uri was set', async function () {
|
||||
expect(await this.token.uri(tokenId)).to.equal(erc1155Uri);
|
||||
});
|
||||
|
||||
it('can request the token uri, returning the concatenated uri if a token uri was set', async function () {
|
||||
const tokenUri = '1234/';
|
||||
const expectedUri = `${baseUri}${tokenUri}`;
|
||||
|
||||
await expect(this.token.$_setURI(ethers.Typed.uint256(tokenId), tokenUri))
|
||||
.to.emit(this.token, 'URI')
|
||||
.withArgs(expectedUri, tokenId);
|
||||
|
||||
expect(await this.token.uri(tokenId)).to.equal(expectedUri);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with base uri set to the empty string', function () {
|
||||
async function fixture() {
|
||||
const [holder] = await ethers.getSigners();
|
||||
|
||||
const token = await ethers.deployContract('$ERC1155URIStorage', ['']);
|
||||
await token.$_mint(holder, tokenId, value, '0x');
|
||||
|
||||
return { token, holder };
|
||||
}
|
||||
|
||||
beforeEach(async function () {
|
||||
Object.assign(this, await loadFixture(fixture));
|
||||
});
|
||||
|
||||
it('can request the token uri, returning an empty string if no token uri was set', async function () {
|
||||
expect(await this.token.uri(tokenId)).to.equal('');
|
||||
});
|
||||
|
||||
it('can request the token uri, returning the token uri if a token uri was set', async function () {
|
||||
const tokenUri = 'ipfs://1234/';
|
||||
|
||||
await expect(this.token.$_setURI(ethers.Typed.uint256(tokenId), tokenUri))
|
||||
.to.emit(this.token, 'URI')
|
||||
.withArgs(tokenUri, tokenId);
|
||||
|
||||
expect(await this.token.uri(tokenId)).to.equal(tokenUri);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,56 @@
|
||||
const { ethers } = require('hardhat');
|
||||
const { expect } = require('chai');
|
||||
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
|
||||
|
||||
const { shouldSupportInterfaces } = require('../../../utils/introspection/SupportsInterface.behavior');
|
||||
|
||||
const ids = [1n, 2n, 3n];
|
||||
const values = [1000n, 2000n, 3000n];
|
||||
const data = '0x12345678';
|
||||
|
||||
async function fixture() {
|
||||
const [owner] = await ethers.getSigners();
|
||||
|
||||
const token = await ethers.deployContract('$ERC1155', ['https://token-cdn-domain/{id}.json']);
|
||||
const mock = await ethers.deployContract('$ERC1155Holder');
|
||||
|
||||
await token.$_mintBatch(owner, ids, values, '0x');
|
||||
|
||||
return { owner, token, mock };
|
||||
}
|
||||
|
||||
describe('ERC1155Holder', function () {
|
||||
beforeEach(async function () {
|
||||
Object.assign(this, await loadFixture(fixture));
|
||||
});
|
||||
|
||||
shouldSupportInterfaces(['ERC1155Receiver']);
|
||||
|
||||
it('receives ERC1155 tokens from a single ID', async function () {
|
||||
await this.token.connect(this.owner).safeTransferFrom(this.owner, this.mock, ids[0], values[0], data);
|
||||
|
||||
expect(await this.token.balanceOf(this.mock, ids[0])).to.equal(values[0]);
|
||||
|
||||
for (let i = 1; i < ids.length; i++) {
|
||||
expect(await this.token.balanceOf(this.mock, ids[i])).to.equal(0n);
|
||||
}
|
||||
});
|
||||
|
||||
it('receives ERC1155 tokens from a multiple IDs', async function () {
|
||||
expect(
|
||||
await this.token.balanceOfBatch(
|
||||
ids.map(() => this.mock),
|
||||
ids,
|
||||
),
|
||||
).to.deep.equal(ids.map(() => 0n));
|
||||
|
||||
await this.token.connect(this.owner).safeBatchTransferFrom(this.owner, this.mock, ids, values, data);
|
||||
|
||||
expect(
|
||||
await this.token.balanceOfBatch(
|
||||
ids.map(() => this.mock),
|
||||
ids,
|
||||
),
|
||||
).to.deep.equal(values);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,299 @@
|
||||
const { ethers } = require('hardhat');
|
||||
const { expect } = require('chai');
|
||||
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
|
||||
const { RevertType } = require('../../../helpers/enums');
|
||||
const { PANIC_CODES } = require('@nomicfoundation/hardhat-chai-matchers/panic');
|
||||
|
||||
const firstTokenId = 1n;
|
||||
const secondTokenId = 2n;
|
||||
const firstTokenValue = 1000n;
|
||||
const secondTokenValue = 1000n;
|
||||
|
||||
const RECEIVER_SINGLE_MAGIC_VALUE = '0xf23a6e61';
|
||||
const RECEIVER_BATCH_MAGIC_VALUE = '0xbc197c81';
|
||||
|
||||
const deployReceiver = (
|
||||
revertType,
|
||||
returnValueSingle = RECEIVER_SINGLE_MAGIC_VALUE,
|
||||
returnValueBatched = RECEIVER_BATCH_MAGIC_VALUE,
|
||||
) => ethers.deployContract('$ERC1155ReceiverMock', [returnValueSingle, returnValueBatched, revertType]);
|
||||
|
||||
const fixture = async () => {
|
||||
const [eoa, operator, owner] = await ethers.getSigners();
|
||||
const utils = await ethers.deployContract('$ERC1155Utils');
|
||||
|
||||
const receivers = {
|
||||
correct: await deployReceiver(RevertType.None),
|
||||
invalid: await deployReceiver(RevertType.None, '0xdeadbeef', '0xdeadbeef'),
|
||||
message: await deployReceiver(RevertType.RevertWithMessage),
|
||||
empty: await deployReceiver(RevertType.RevertWithoutMessage),
|
||||
customError: await deployReceiver(RevertType.RevertWithCustomError),
|
||||
panic: await deployReceiver(RevertType.Panic),
|
||||
nonReceiver: await ethers.deployContract('CallReceiverMock'),
|
||||
eoa,
|
||||
};
|
||||
|
||||
return { operator, owner, utils, receivers };
|
||||
};
|
||||
|
||||
describe('ERC1155Utils', function () {
|
||||
beforeEach(async function () {
|
||||
Object.assign(this, await loadFixture(fixture));
|
||||
});
|
||||
|
||||
describe('onERC1155Received', function () {
|
||||
it('succeeds when called by an EOA', async function () {
|
||||
await expect(
|
||||
this.utils.$checkOnERC1155Received(
|
||||
this.operator,
|
||||
this.owner,
|
||||
this.receivers.eoa,
|
||||
firstTokenId,
|
||||
firstTokenValue,
|
||||
'0x',
|
||||
),
|
||||
).to.not.be.reverted;
|
||||
});
|
||||
|
||||
it('succeeds when data is passed', async function () {
|
||||
const data = '0x12345678';
|
||||
await expect(
|
||||
this.utils.$checkOnERC1155Received(
|
||||
this.operator,
|
||||
this.owner,
|
||||
this.receivers.correct,
|
||||
firstTokenId,
|
||||
firstTokenValue,
|
||||
data,
|
||||
),
|
||||
).to.not.be.reverted;
|
||||
});
|
||||
|
||||
it('succeeds when data is empty', async function () {
|
||||
await expect(
|
||||
this.utils.$checkOnERC1155Received(
|
||||
this.operator,
|
||||
this.owner,
|
||||
this.receivers.correct,
|
||||
firstTokenId,
|
||||
firstTokenValue,
|
||||
'0x',
|
||||
),
|
||||
).to.not.be.reverted;
|
||||
});
|
||||
|
||||
it('reverts when receiver returns invalid value', async function () {
|
||||
await expect(
|
||||
this.utils.$checkOnERC1155Received(
|
||||
this.operator,
|
||||
this.owner,
|
||||
this.receivers.invalid,
|
||||
firstTokenId,
|
||||
firstTokenValue,
|
||||
'0x',
|
||||
),
|
||||
)
|
||||
.to.be.revertedWithCustomError(this.utils, 'ERC1155InvalidReceiver')
|
||||
.withArgs(this.receivers.invalid);
|
||||
});
|
||||
|
||||
it('reverts when receiver reverts with message', async function () {
|
||||
await expect(
|
||||
this.utils.$checkOnERC1155Received(
|
||||
this.operator,
|
||||
this.owner,
|
||||
this.receivers.message,
|
||||
firstTokenId,
|
||||
firstTokenValue,
|
||||
'0x',
|
||||
),
|
||||
).to.be.revertedWith('ERC1155ReceiverMock: reverting on receive');
|
||||
});
|
||||
|
||||
it('reverts when receiver reverts without message', async function () {
|
||||
await expect(
|
||||
this.utils.$checkOnERC1155Received(
|
||||
this.operator,
|
||||
this.owner,
|
||||
this.receivers.empty,
|
||||
firstTokenId,
|
||||
firstTokenValue,
|
||||
'0x',
|
||||
),
|
||||
)
|
||||
.to.be.revertedWithCustomError(this.utils, 'ERC1155InvalidReceiver')
|
||||
.withArgs(this.receivers.empty);
|
||||
});
|
||||
|
||||
it('reverts when receiver reverts with custom error', async function () {
|
||||
await expect(
|
||||
this.utils.$checkOnERC1155Received(
|
||||
this.operator,
|
||||
this.owner,
|
||||
this.receivers.customError,
|
||||
firstTokenId,
|
||||
firstTokenValue,
|
||||
'0x',
|
||||
),
|
||||
)
|
||||
.to.be.revertedWithCustomError(this.receivers.customError, 'CustomError')
|
||||
.withArgs(RECEIVER_SINGLE_MAGIC_VALUE);
|
||||
});
|
||||
|
||||
it('reverts when receiver panics', async function () {
|
||||
await expect(
|
||||
this.utils.$checkOnERC1155Received(
|
||||
this.operator,
|
||||
this.owner,
|
||||
this.receivers.panic,
|
||||
firstTokenId,
|
||||
firstTokenValue,
|
||||
'0x',
|
||||
),
|
||||
).to.be.revertedWithPanic(PANIC_CODES.DIVISION_BY_ZERO);
|
||||
});
|
||||
|
||||
it('reverts when receiver does not implement onERC1155Received', async function () {
|
||||
await expect(
|
||||
this.utils.$checkOnERC1155Received(
|
||||
this.operator,
|
||||
this.owner,
|
||||
this.receivers.nonReceiver,
|
||||
firstTokenId,
|
||||
firstTokenValue,
|
||||
'0x',
|
||||
),
|
||||
)
|
||||
.to.be.revertedWithCustomError(this.utils, 'ERC1155InvalidReceiver')
|
||||
.withArgs(this.receivers.nonReceiver);
|
||||
});
|
||||
});
|
||||
|
||||
describe('onERC1155BatchReceived', function () {
|
||||
it('succeeds when called by an EOA', async function () {
|
||||
await expect(
|
||||
this.utils.$checkOnERC1155BatchReceived(
|
||||
this.operator,
|
||||
this.owner,
|
||||
this.receivers.eoa,
|
||||
[firstTokenId, secondTokenId],
|
||||
[firstTokenValue, secondTokenValue],
|
||||
'0x',
|
||||
),
|
||||
).to.not.be.reverted;
|
||||
});
|
||||
|
||||
it('succeeds when data is passed', async function () {
|
||||
const data = '0x12345678';
|
||||
await expect(
|
||||
this.utils.$checkOnERC1155BatchReceived(
|
||||
this.operator,
|
||||
this.owner,
|
||||
this.receivers.correct,
|
||||
[firstTokenId, secondTokenId],
|
||||
[firstTokenValue, secondTokenValue],
|
||||
data,
|
||||
),
|
||||
).to.not.be.reverted;
|
||||
});
|
||||
|
||||
it('succeeds when data is empty', async function () {
|
||||
await expect(
|
||||
this.utils.$checkOnERC1155BatchReceived(
|
||||
this.operator,
|
||||
this.owner,
|
||||
this.receivers.correct,
|
||||
[firstTokenId, secondTokenId],
|
||||
[firstTokenValue, secondTokenValue],
|
||||
'0x',
|
||||
),
|
||||
).to.not.be.reverted;
|
||||
});
|
||||
|
||||
it('reverts when receiver returns invalid value', async function () {
|
||||
await expect(
|
||||
this.utils.$checkOnERC1155BatchReceived(
|
||||
this.operator,
|
||||
this.owner,
|
||||
this.receivers.invalid,
|
||||
[firstTokenId, secondTokenId],
|
||||
[firstTokenValue, secondTokenValue],
|
||||
'0x',
|
||||
),
|
||||
)
|
||||
.to.be.revertedWithCustomError(this.utils, 'ERC1155InvalidReceiver')
|
||||
.withArgs(this.receivers.invalid);
|
||||
});
|
||||
|
||||
it('reverts when receiver reverts with message', async function () {
|
||||
await expect(
|
||||
this.utils.$checkOnERC1155BatchReceived(
|
||||
this.operator,
|
||||
this.owner,
|
||||
this.receivers.message,
|
||||
[firstTokenId, secondTokenId],
|
||||
[firstTokenValue, secondTokenValue],
|
||||
'0x',
|
||||
),
|
||||
).to.be.revertedWith('ERC1155ReceiverMock: reverting on batch receive');
|
||||
});
|
||||
|
||||
it('reverts when receiver reverts without message', async function () {
|
||||
await expect(
|
||||
this.utils.$checkOnERC1155BatchReceived(
|
||||
this.operator,
|
||||
this.owner,
|
||||
this.receivers.empty,
|
||||
[firstTokenId, secondTokenId],
|
||||
[firstTokenValue, secondTokenValue],
|
||||
'0x',
|
||||
),
|
||||
)
|
||||
.to.be.revertedWithCustomError(this.utils, 'ERC1155InvalidReceiver')
|
||||
.withArgs(this.receivers.empty);
|
||||
});
|
||||
|
||||
it('reverts when receiver reverts with custom error', async function () {
|
||||
await expect(
|
||||
this.utils.$checkOnERC1155BatchReceived(
|
||||
this.operator,
|
||||
this.owner,
|
||||
this.receivers.customError,
|
||||
[firstTokenId, secondTokenId],
|
||||
[firstTokenValue, secondTokenValue],
|
||||
'0x',
|
||||
),
|
||||
)
|
||||
.to.be.revertedWithCustomError(this.receivers.customError, 'CustomError')
|
||||
.withArgs(RECEIVER_SINGLE_MAGIC_VALUE);
|
||||
});
|
||||
|
||||
it('reverts when receiver panics', async function () {
|
||||
await expect(
|
||||
this.utils.$checkOnERC1155BatchReceived(
|
||||
this.operator,
|
||||
this.owner,
|
||||
this.receivers.panic,
|
||||
[firstTokenId, secondTokenId],
|
||||
[firstTokenValue, secondTokenValue],
|
||||
'0x',
|
||||
),
|
||||
).to.be.revertedWithPanic(PANIC_CODES.DIVISION_BY_ZERO);
|
||||
});
|
||||
|
||||
it('reverts when receiver does not implement onERC1155BatchReceived', async function () {
|
||||
await expect(
|
||||
this.utils.$checkOnERC1155BatchReceived(
|
||||
this.operator,
|
||||
this.owner,
|
||||
this.receivers.nonReceiver,
|
||||
[firstTokenId, secondTokenId],
|
||||
[firstTokenValue, secondTokenValue],
|
||||
'0x',
|
||||
),
|
||||
)
|
||||
.to.be.revertedWithCustomError(this.utils, 'ERC1155InvalidReceiver')
|
||||
.withArgs(this.receivers.nonReceiver);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user