1159 lines
44 KiB
TypeScript
1159 lines
44 KiB
TypeScript
|
import { expect } from 'chai';
|
||
|
import { ethers } from 'hardhat';
|
||
|
import { BigNumber, constants } from 'ethers';
|
||
|
import { NestableTokenMock } from '../typechain-types';
|
||
|
import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers';
|
||
|
import { loadFixture } from '@nomicfoundation/hardhat-network-helpers';
|
||
|
|
||
|
function bn(x: number): BigNumber {
|
||
|
return BigNumber.from(x);
|
||
|
}
|
||
|
|
||
|
const ADDRESS_ZERO = constants.AddressZero;
|
||
|
|
||
|
async function parentChildFixture(): Promise<{
|
||
|
parent: NestableTokenMock;
|
||
|
child: NestableTokenMock;
|
||
|
}> {
|
||
|
const factory = await ethers.getContractFactory('NestableTokenMock');
|
||
|
|
||
|
const parent = await factory.deploy();
|
||
|
await parent.deployed();
|
||
|
const child = await factory.deploy();
|
||
|
await child.deployed();
|
||
|
return { parent, child };
|
||
|
}
|
||
|
|
||
|
describe('NestableToken', function () {
|
||
|
let parent: NestableTokenMock;
|
||
|
let child: NestableTokenMock;
|
||
|
let owner: SignerWithAddress;
|
||
|
let tokenOwner: SignerWithAddress;
|
||
|
let addrs: SignerWithAddress[];
|
||
|
|
||
|
beforeEach(async function () {
|
||
|
[owner, tokenOwner, ...addrs] = await ethers.getSigners();
|
||
|
({ parent, child } = await loadFixture(parentChildFixture));
|
||
|
});
|
||
|
|
||
|
describe('Minting', async function () {
|
||
|
it('cannot mint id 0', async function () {
|
||
|
const tokenId1 = 0;
|
||
|
await expect(child.mint(owner.address, tokenId1)).to.be.revertedWithCustomError(
|
||
|
child,
|
||
|
'IdZeroForbidden',
|
||
|
);
|
||
|
});
|
||
|
|
||
|
it('cannot nest mint id 0', async function () {
|
||
|
const parentId = 1;
|
||
|
await child.mint(owner.address, parentId);
|
||
|
const childId1 = 0;
|
||
|
await expect(
|
||
|
child.nestMint(parent.address, childId1, parentId),
|
||
|
).to.be.revertedWithCustomError(child, 'IdZeroForbidden');
|
||
|
});
|
||
|
|
||
|
it('cannot mint already minted token', async function () {
|
||
|
const tokenId1 = 1;
|
||
|
await child.mint(owner.address, tokenId1);
|
||
|
await expect(child.mint(owner.address, tokenId1)).to.be.revertedWithCustomError(
|
||
|
child,
|
||
|
'ERC721TokenAlreadyMinted',
|
||
|
);
|
||
|
});
|
||
|
|
||
|
it('cannot nest mint already minted token', async function () {
|
||
|
const parentId = 1;
|
||
|
const childId1 = 99;
|
||
|
await parent.mint(owner.address, parentId);
|
||
|
await child.nestMint(parent.address, childId1, parentId);
|
||
|
|
||
|
await expect(
|
||
|
child.nestMint(parent.address, childId1, parentId),
|
||
|
).to.be.revertedWithCustomError(child, 'ERC721TokenAlreadyMinted');
|
||
|
});
|
||
|
|
||
|
it('cannot nest mint already minted token', async function () {
|
||
|
const parentId = 1;
|
||
|
const childId1 = 99;
|
||
|
await parent.mint(owner.address, parentId);
|
||
|
await child.nestMint(parent.address, childId1, parentId);
|
||
|
|
||
|
await expect(
|
||
|
child.nestMint(parent.address, childId1, parentId),
|
||
|
).to.be.revertedWithCustomError(child, 'ERC721TokenAlreadyMinted');
|
||
|
});
|
||
|
|
||
|
it('can mint with no destination', async function () {
|
||
|
const tokenId1 = 1;
|
||
|
await child.mint(tokenOwner.address, tokenId1);
|
||
|
expect(await child.ownerOf(tokenId1)).to.equal(tokenOwner.address);
|
||
|
expect(await child.directOwnerOf(tokenId1)).to.eql([tokenOwner.address, bn(0), false]);
|
||
|
});
|
||
|
|
||
|
it('has right owners', async function () {
|
||
|
const otherOwner = addrs[2];
|
||
|
const tokenId1 = 1;
|
||
|
await parent.mint(tokenOwner.address, tokenId1);
|
||
|
const tokenId2 = 2;
|
||
|
await parent.mint(otherOwner.address, tokenId2);
|
||
|
const tokenId3 = 3;
|
||
|
await parent.mint(otherOwner.address, tokenId3);
|
||
|
|
||
|
expect(await parent.ownerOf(tokenId1)).to.equal(tokenOwner.address);
|
||
|
expect(await parent.ownerOf(tokenId2)).to.equal(otherOwner.address);
|
||
|
expect(await parent.ownerOf(tokenId3)).to.equal(otherOwner.address);
|
||
|
|
||
|
expect(await parent.balanceOf(tokenOwner.address)).to.equal(1);
|
||
|
expect(await parent.balanceOf(otherOwner.address)).to.equal(2);
|
||
|
|
||
|
await expect(parent.ownerOf(9999)).to.be.revertedWithCustomError(
|
||
|
parent,
|
||
|
'ERC721InvalidTokenId',
|
||
|
);
|
||
|
});
|
||
|
|
||
|
it('cannot mint to zero address', async function () {
|
||
|
await expect(child.mint(ADDRESS_ZERO, 1)).to.be.revertedWithCustomError(
|
||
|
child,
|
||
|
'ERC721MintToTheZeroAddress',
|
||
|
);
|
||
|
});
|
||
|
|
||
|
it('cannot nest mint to a non-contract destination', async function () {
|
||
|
await expect(child.nestMint(tokenOwner.address, 1, 1)).to.be.revertedWithCustomError(
|
||
|
child,
|
||
|
'IsNotContract',
|
||
|
);
|
||
|
});
|
||
|
|
||
|
it('cannot nest mint to non nestable receiver', async function () {
|
||
|
const ERC721 = await ethers.getContractFactory('ERC721Mock');
|
||
|
const nonReceiver = await ERC721.deploy('Non receiver', 'NR');
|
||
|
await nonReceiver.deployed();
|
||
|
|
||
|
await expect(child.nestMint(nonReceiver.address, 1, 1)).to.be.revertedWithCustomError(
|
||
|
child,
|
||
|
'MintToNonNestableImplementer',
|
||
|
);
|
||
|
});
|
||
|
|
||
|
it('cannot nest mint to a non-existent token', async function () {
|
||
|
await expect(child.nestMint(parent.address, 1, 1)).to.be.revertedWithCustomError(
|
||
|
child,
|
||
|
'ERC721InvalidTokenId',
|
||
|
);
|
||
|
});
|
||
|
|
||
|
it('cannot nest mint to zero address', async function () {
|
||
|
const parentId = 1;
|
||
|
await parent.mint(tokenOwner.address, parentId);
|
||
|
await expect(child.nestMint(ADDRESS_ZERO, parentId, 1)).to.be.revertedWithCustomError(
|
||
|
child,
|
||
|
'IsNotContract',
|
||
|
);
|
||
|
});
|
||
|
|
||
|
it('can mint to contract and owners are ok', async function () {
|
||
|
const parentId = 1;
|
||
|
await parent.mint(tokenOwner.address, parentId);
|
||
|
const childId1 = 99;
|
||
|
await child.nestMint(parent.address, childId1, parentId);
|
||
|
|
||
|
// owner is the same adress
|
||
|
expect(await parent.ownerOf(parentId)).to.equal(tokenOwner.address);
|
||
|
expect(await child.ownerOf(childId1)).to.equal(tokenOwner.address);
|
||
|
|
||
|
expect(await parent.balanceOf(tokenOwner.address)).to.equal(1);
|
||
|
expect(await child.balanceOf(parent.address)).to.equal(1);
|
||
|
});
|
||
|
|
||
|
it('can mint to contract and direct owners are ok', async function () {
|
||
|
const parentId = 1;
|
||
|
await parent.mint(tokenOwner.address, parentId);
|
||
|
const childId1 = 99;
|
||
|
await child.nestMint(parent.address, childId1, parentId);
|
||
|
|
||
|
// Direct owner is an address for the parent
|
||
|
expect(await parent.directOwnerOf(parentId)).to.eql([tokenOwner.address, bn(0), false]);
|
||
|
// Direct owner is a contract for the child
|
||
|
expect(await child.directOwnerOf(childId1)).to.eql([parent.address, bn(parentId), true]);
|
||
|
});
|
||
|
|
||
|
it("can mint to contract and parent's children are ok", async function () {
|
||
|
const parentId = 1;
|
||
|
await parent.mint(tokenOwner.address, parentId);
|
||
|
const childId1 = 99;
|
||
|
await child.nestMint(parent.address, childId1, parentId);
|
||
|
|
||
|
const children = await parent.childrenOf(parentId);
|
||
|
expect(children).to.eql([]);
|
||
|
|
||
|
const pendingChildren = await parent.pendingChildrenOf(parentId);
|
||
|
expect(pendingChildren).to.eql([[bn(childId1), child.address]]);
|
||
|
expect(await parent.pendingChildOf(parentId, 0)).to.eql([bn(childId1), child.address]);
|
||
|
});
|
||
|
|
||
|
it('cannot get child out of index', async function () {
|
||
|
const parentId = 1;
|
||
|
await parent.mint(tokenOwner.address, parentId);
|
||
|
await expect(parent.childOf(parentId, 0)).to.be.revertedWithCustomError(
|
||
|
parent,
|
||
|
'ChildIndexOutOfRange',
|
||
|
);
|
||
|
});
|
||
|
|
||
|
it('cannot get pending child out of index', async function () {
|
||
|
const parentId = 1;
|
||
|
await parent.mint(tokenOwner.address, parentId);
|
||
|
await expect(parent.pendingChildOf(parentId, 0)).to.be.revertedWithCustomError(
|
||
|
parent,
|
||
|
'PendingChildIndexOutOfRange',
|
||
|
);
|
||
|
});
|
||
|
|
||
|
it('can mint multiple children', async function () {
|
||
|
const parentId = 1;
|
||
|
const childId1 = 99;
|
||
|
const childId2 = 100;
|
||
|
await parent.mint(tokenOwner.address, parentId);
|
||
|
await child.nestMint(parent.address, childId1, parentId);
|
||
|
await child.nestMint(parent.address, childId2, parentId);
|
||
|
|
||
|
expect(await child.ownerOf(childId1)).to.equal(tokenOwner.address);
|
||
|
expect(await child.ownerOf(childId2)).to.equal(tokenOwner.address);
|
||
|
|
||
|
expect(await child.balanceOf(parent.address)).to.equal(2);
|
||
|
|
||
|
const pendingChildren = await parent.pendingChildrenOf(parentId);
|
||
|
expect(pendingChildren).to.eql([
|
||
|
[bn(childId1), child.address],
|
||
|
[bn(childId2), child.address],
|
||
|
]);
|
||
|
});
|
||
|
|
||
|
it('can mint child into child', async function () {
|
||
|
const parentId = 1;
|
||
|
const childId1 = 99;
|
||
|
const granchildId = 999;
|
||
|
await parent.mint(tokenOwner.address, parentId);
|
||
|
await child.nestMint(parent.address, childId1, parentId);
|
||
|
await child.nestMint(child.address, granchildId, childId1);
|
||
|
|
||
|
// Check balances -- yes, technically the counted balance indicates `child` owns an instance of itself
|
||
|
// and this is a little counterintuitive, but the root owner is the EOA.
|
||
|
expect(await child.balanceOf(parent.address)).to.equal(1);
|
||
|
expect(await child.balanceOf(child.address)).to.equal(1);
|
||
|
|
||
|
const pendingChildrenOfChunky10 = await parent.pendingChildrenOf(parentId);
|
||
|
const pendingChildrenOfMonkey1 = await child.pendingChildrenOf(childId1);
|
||
|
|
||
|
expect(pendingChildrenOfChunky10).to.eql([[bn(childId1), child.address]]);
|
||
|
expect(pendingChildrenOfMonkey1).to.eql([[bn(granchildId), child.address]]);
|
||
|
|
||
|
expect(await child.directOwnerOf(granchildId)).to.eql([child.address, bn(childId1), true]);
|
||
|
|
||
|
expect(await child.ownerOf(granchildId)).to.eql(tokenOwner.address);
|
||
|
});
|
||
|
|
||
|
it('cannot have too many pending children', async () => {
|
||
|
const parentId = 1;
|
||
|
await parent.mint(tokenOwner.address, parentId);
|
||
|
|
||
|
// First 128 should be fine.
|
||
|
for (let i = 1; i <= 128; i++) {
|
||
|
await child.nestMint(parent.address, i, parentId);
|
||
|
}
|
||
|
|
||
|
await expect(child.nestMint(parent.address, 129, parentId)).to.be.revertedWithCustomError(
|
||
|
child,
|
||
|
'MaxPendingChildrenReached',
|
||
|
);
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('Interface support', async function () {
|
||
|
it('can support IERC165', async function () {
|
||
|
expect(await parent.supportsInterface('0x01ffc9a7')).to.equal(true);
|
||
|
});
|
||
|
|
||
|
it('can support IERC721', async function () {
|
||
|
expect(await parent.supportsInterface('0x80ac58cd')).to.equal(true);
|
||
|
});
|
||
|
|
||
|
it('can support IERC6059', async function () {
|
||
|
expect(await parent.supportsInterface('0x42b0e56f')).to.equal(true);
|
||
|
});
|
||
|
|
||
|
it('cannot support other interfaceId', async function () {
|
||
|
expect(await parent.supportsInterface('0xffffffff')).to.equal(false);
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('Adding child', async function () {
|
||
|
it('cannot add child from user address', async function () {
|
||
|
const tokenOwner1 = addrs[0];
|
||
|
const tokenOwner2 = addrs[1];
|
||
|
const parentId = 1;
|
||
|
await parent.mint(tokenOwner1.address, parentId);
|
||
|
const childId1 = 99;
|
||
|
await child.mint(tokenOwner2.address, childId1);
|
||
|
await expect(parent.addChild(parentId, childId1, '0x')).to.be.revertedWithCustomError(
|
||
|
parent,
|
||
|
'IsNotContract',
|
||
|
);
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('Accept child', async function () {
|
||
|
let parentId: number;
|
||
|
let childId1: number;
|
||
|
|
||
|
beforeEach(async function () {
|
||
|
parentId = 1;
|
||
|
await parent.mint(tokenOwner.address, parentId);
|
||
|
childId1 = 99;
|
||
|
await child.nestMint(parent.address, childId1, parentId);
|
||
|
});
|
||
|
|
||
|
it('can accept child', async function () {
|
||
|
await expect(parent.connect(tokenOwner).acceptChild(parentId, 0, child.address, childId1))
|
||
|
.to.emit(parent, 'ChildAccepted')
|
||
|
.withArgs(parentId, 0, child.address, childId1);
|
||
|
await checkChildWasAccepted();
|
||
|
});
|
||
|
|
||
|
it('can accept child if approved', async function () {
|
||
|
const approved = addrs[1];
|
||
|
await parent.connect(tokenOwner).approve(approved.address, parentId);
|
||
|
await parent.connect(approved).acceptChild(parentId, 0, child.address, childId1);
|
||
|
await checkChildWasAccepted();
|
||
|
});
|
||
|
|
||
|
it('can accept child if approved for all', async function () {
|
||
|
const operator = addrs[2];
|
||
|
await parent.connect(tokenOwner).setApprovalForAll(operator.address, true);
|
||
|
await parent.connect(operator).acceptChild(parentId, 0, child.address, childId1);
|
||
|
await checkChildWasAccepted();
|
||
|
});
|
||
|
|
||
|
it('cannot accept not owned child', async function () {
|
||
|
const notOwner = addrs[3];
|
||
|
await expect(
|
||
|
parent.connect(notOwner).acceptChild(parentId, 0, child.address, childId1),
|
||
|
).to.be.revertedWithCustomError(parent, 'ERC721NotApprovedOrOwner');
|
||
|
});
|
||
|
|
||
|
it('cannot accept child if address or id do not match', async function () {
|
||
|
const otherAddress = addrs[1].address;
|
||
|
const otherChildId = 9999;
|
||
|
await expect(
|
||
|
parent.connect(tokenOwner).acceptChild(parentId, 0, child.address, otherChildId),
|
||
|
).to.be.revertedWithCustomError(parent, 'UnexpectedChildId');
|
||
|
await expect(
|
||
|
parent.connect(tokenOwner).acceptChild(parentId, 0, otherAddress, childId1),
|
||
|
).to.be.revertedWithCustomError(parent, 'UnexpectedChildId');
|
||
|
});
|
||
|
|
||
|
it('cannot accept children for non existing index', async () => {
|
||
|
await expect(
|
||
|
parent.connect(tokenOwner).acceptChild(parentId, 1, child.address, childId1),
|
||
|
).to.be.revertedWithCustomError(parent, 'PendingChildIndexOutOfRange');
|
||
|
});
|
||
|
|
||
|
async function checkChildWasAccepted() {
|
||
|
expect(await parent.pendingChildrenOf(parentId)).to.eql([]);
|
||
|
expect(await parent.childrenOf(parentId)).to.eql([[bn(childId1), child.address]]);
|
||
|
}
|
||
|
});
|
||
|
|
||
|
describe('Rejecting children', async function () {
|
||
|
let parentId: number;
|
||
|
|
||
|
beforeEach(async function () {
|
||
|
parentId = 1;
|
||
|
await parent.mint(tokenOwner.address, parentId);
|
||
|
await child.nestMint(parent.address, 99, parentId);
|
||
|
});
|
||
|
|
||
|
it('can reject all pending children', async function () {
|
||
|
// Mint a couple of more children
|
||
|
await child.nestMint(parent.address, 100, parentId);
|
||
|
await child.nestMint(parent.address, 101, parentId);
|
||
|
|
||
|
await expect(parent.connect(tokenOwner).rejectAllChildren(parentId, 3))
|
||
|
.to.emit(parent, 'AllChildrenRejected')
|
||
|
.withArgs(parentId);
|
||
|
await checkNoChildrenNorPending(parentId);
|
||
|
|
||
|
// They are still on the child
|
||
|
expect(await child.balanceOf(parent.address)).to.equal(3);
|
||
|
});
|
||
|
|
||
|
it('cannot reject all pending children if there are more than expected', async function () {
|
||
|
// Mint a couple of more children
|
||
|
await child.nestMint(parent.address, 100, parentId);
|
||
|
await child.nestMint(parent.address, 101, parentId);
|
||
|
|
||
|
await expect(
|
||
|
parent.connect(tokenOwner).rejectAllChildren(parentId, 1),
|
||
|
).to.be.revertedWithCustomError(parent, 'UnexpectedNumberOfChildren');
|
||
|
});
|
||
|
|
||
|
it('can reject all pending children if approved', async function () {
|
||
|
// Mint a couple of more children
|
||
|
await child.nestMint(parent.address, 100, parentId);
|
||
|
await child.nestMint(parent.address, 101, parentId);
|
||
|
|
||
|
const rejecter = addrs[1];
|
||
|
await parent.connect(tokenOwner).approve(rejecter.address, parentId);
|
||
|
await parent.connect(rejecter).rejectAllChildren(parentId, 3);
|
||
|
await checkNoChildrenNorPending(parentId);
|
||
|
});
|
||
|
|
||
|
it('can reject all pending children if approved for all', async function () {
|
||
|
// Mint a couple of more children
|
||
|
await child.nestMint(parent.address, 100, parentId);
|
||
|
await child.nestMint(parent.address, 101, parentId);
|
||
|
|
||
|
const operator = addrs[2];
|
||
|
await parent.connect(tokenOwner).setApprovalForAll(operator.address, true);
|
||
|
await parent.connect(operator).rejectAllChildren(parentId, 3);
|
||
|
await checkNoChildrenNorPending(parentId);
|
||
|
});
|
||
|
|
||
|
it('cannot reject all pending children for not owned pending child', async function () {
|
||
|
const notOwner = addrs[3];
|
||
|
|
||
|
await expect(
|
||
|
parent.connect(notOwner).rejectAllChildren(parentId, 2),
|
||
|
).to.be.revertedWithCustomError(parent, 'ERC721NotApprovedOrOwner');
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('Burning', async function () {
|
||
|
let parentId: number;
|
||
|
|
||
|
beforeEach(async function () {
|
||
|
parentId = 1;
|
||
|
await parent.mint(tokenOwner.address, parentId);
|
||
|
});
|
||
|
|
||
|
it('can burn token', async function () {
|
||
|
expect(await parent.balanceOf(tokenOwner.address)).to.equal(1);
|
||
|
await parent.connect(tokenOwner)['burn(uint256)'](parentId);
|
||
|
await checkBurntParent();
|
||
|
});
|
||
|
|
||
|
it('can burn token if approved', async function () {
|
||
|
const approved = addrs[1];
|
||
|
await parent.connect(tokenOwner).approve(approved.address, parentId);
|
||
|
await parent.connect(approved)['burn(uint256)'](parentId);
|
||
|
await checkBurntParent();
|
||
|
});
|
||
|
|
||
|
it('can burn token if approved for all', async function () {
|
||
|
const operator = addrs[2];
|
||
|
await parent.connect(tokenOwner).setApprovalForAll(operator.address, true);
|
||
|
await parent.connect(operator)['burn(uint256)'](parentId);
|
||
|
await checkBurntParent();
|
||
|
});
|
||
|
|
||
|
it('can recursively burn nested token', async function () {
|
||
|
const childId1 = 99;
|
||
|
const granchildId = 999;
|
||
|
await child.nestMint(parent.address, childId1, parentId);
|
||
|
await child.nestMint(child.address, granchildId, childId1);
|
||
|
await parent.connect(tokenOwner).acceptChild(parentId, 0, child.address, childId1);
|
||
|
await child.connect(tokenOwner).acceptChild(childId1, 0, child.address, granchildId);
|
||
|
|
||
|
expect(await parent.balanceOf(tokenOwner.address)).to.equal(1);
|
||
|
expect(await child.balanceOf(parent.address)).to.equal(1);
|
||
|
expect(await child.balanceOf(child.address)).to.equal(1);
|
||
|
|
||
|
expect(await parent.childrenOf(parentId)).to.eql([[bn(childId1), child.address]]);
|
||
|
expect(await child.childrenOf(childId1)).to.eql([[bn(granchildId), child.address]]);
|
||
|
expect(await child.directOwnerOf(granchildId)).to.eql([child.address, bn(childId1), true]);
|
||
|
|
||
|
// Sets recursive burns to 2
|
||
|
await parent.connect(tokenOwner)['burn(uint256,uint256)'](parentId, 2);
|
||
|
|
||
|
expect(await parent.balanceOf(tokenOwner.address)).to.equal(0);
|
||
|
expect(await child.balanceOf(parent.address)).to.equal(0);
|
||
|
expect(await child.balanceOf(child.address)).to.equal(0);
|
||
|
|
||
|
await expect(parent.ownerOf(parentId)).to.be.revertedWithCustomError(
|
||
|
parent,
|
||
|
'ERC721InvalidTokenId',
|
||
|
);
|
||
|
await expect(parent.directOwnerOf(parentId)).to.be.revertedWithCustomError(
|
||
|
parent,
|
||
|
'ERC721InvalidTokenId',
|
||
|
);
|
||
|
|
||
|
await expect(child.ownerOf(childId1)).to.be.revertedWithCustomError(
|
||
|
child,
|
||
|
'ERC721InvalidTokenId',
|
||
|
);
|
||
|
await expect(child.directOwnerOf(childId1)).to.be.revertedWithCustomError(
|
||
|
child,
|
||
|
'ERC721InvalidTokenId',
|
||
|
);
|
||
|
|
||
|
await expect(parent.ownerOf(granchildId)).to.be.revertedWithCustomError(
|
||
|
parent,
|
||
|
'ERC721InvalidTokenId',
|
||
|
);
|
||
|
await expect(parent.directOwnerOf(granchildId)).to.be.revertedWithCustomError(
|
||
|
parent,
|
||
|
'ERC721InvalidTokenId',
|
||
|
);
|
||
|
});
|
||
|
|
||
|
it('can recursively burn nested token with the right number of recursive burns', async function () {
|
||
|
// Parent
|
||
|
// -> Child1
|
||
|
// -> GrandChild1
|
||
|
// -> GrandChild2
|
||
|
// -> GreatGrandChild1
|
||
|
// -> Child2
|
||
|
// Total tree 5 (4 recursive burns)
|
||
|
const childId1 = 99;
|
||
|
const childId2 = 100;
|
||
|
const grandChild1 = 999;
|
||
|
const grandChild2 = 1000;
|
||
|
const greatGrandChild1 = 9999;
|
||
|
await child.nestMint(parent.address, childId1, parentId);
|
||
|
await child.nestMint(parent.address, childId2, parentId);
|
||
|
await child.nestMint(child.address, grandChild1, childId1);
|
||
|
await child.nestMint(child.address, grandChild2, childId1);
|
||
|
await child.nestMint(child.address, greatGrandChild1, grandChild2);
|
||
|
await parent.connect(tokenOwner).acceptChild(parentId, 0, child.address, childId1);
|
||
|
await parent.connect(tokenOwner).acceptChild(parentId, 0, child.address, childId2);
|
||
|
await child.connect(tokenOwner).acceptChild(childId1, 0, child.address, grandChild1);
|
||
|
await child.connect(tokenOwner).acceptChild(childId1, 0, child.address, grandChild2);
|
||
|
await child.connect(tokenOwner).acceptChild(grandChild2, 0, child.address, greatGrandChild1);
|
||
|
|
||
|
// 0 is not enough
|
||
|
await expect(parent.connect(tokenOwner)['burn(uint256,uint256)'](parentId, 0))
|
||
|
.to.be.revertedWithCustomError(parent, 'MaxRecursiveBurnsReached')
|
||
|
.withArgs(child.address, childId1);
|
||
|
// 1 is not enough
|
||
|
await expect(parent.connect(tokenOwner)['burn(uint256,uint256)'](parentId, 1))
|
||
|
.to.be.revertedWithCustomError(parent, 'MaxRecursiveBurnsReached')
|
||
|
.withArgs(child.address, grandChild1);
|
||
|
// 2 is not enough
|
||
|
await expect(parent.connect(tokenOwner)['burn(uint256,uint256)'](parentId, 2))
|
||
|
.to.be.revertedWithCustomError(parent, 'MaxRecursiveBurnsReached')
|
||
|
.withArgs(child.address, grandChild2);
|
||
|
// 3 is not enough
|
||
|
await expect(parent.connect(tokenOwner)['burn(uint256,uint256)'](parentId, 3))
|
||
|
.to.be.revertedWithCustomError(parent, 'MaxRecursiveBurnsReached')
|
||
|
.withArgs(child.address, greatGrandChild1);
|
||
|
// 4 is not enough
|
||
|
await expect(parent.connect(tokenOwner)['burn(uint256,uint256)'](parentId, 4))
|
||
|
.to.be.revertedWithCustomError(parent, 'MaxRecursiveBurnsReached')
|
||
|
.withArgs(child.address, childId2);
|
||
|
// 5 is just enough
|
||
|
await parent.connect(tokenOwner)['burn(uint256,uint256)'](parentId, 5);
|
||
|
});
|
||
|
|
||
|
async function checkBurntParent() {
|
||
|
expect(await parent.balanceOf(addrs[1].address)).to.equal(0);
|
||
|
await expect(parent.ownerOf(parentId)).to.be.revertedWithCustomError(
|
||
|
parent,
|
||
|
'ERC721InvalidTokenId',
|
||
|
);
|
||
|
}
|
||
|
});
|
||
|
|
||
|
describe('Transferring Active Children', async function () {
|
||
|
let parentId: number;
|
||
|
let childId1: number;
|
||
|
|
||
|
beforeEach(async function () {
|
||
|
parentId = 1;
|
||
|
childId1 = 99;
|
||
|
await parent.mint(tokenOwner.address, parentId);
|
||
|
await child.nestMint(parent.address, childId1, parentId);
|
||
|
await parent.connect(tokenOwner).acceptChild(parentId, 0, child.address, childId1);
|
||
|
});
|
||
|
|
||
|
it('can transfer child with to as root owner', async function () {
|
||
|
await expect(
|
||
|
parent
|
||
|
.connect(tokenOwner)
|
||
|
.transferChild(parentId, tokenOwner.address, 0, 0, child.address, childId1, false, '0x'),
|
||
|
)
|
||
|
.to.emit(parent, 'ChildTransferred')
|
||
|
.withArgs(parentId, 0, child.address, childId1, false);
|
||
|
|
||
|
await checkChildMovedToRootOwner();
|
||
|
});
|
||
|
|
||
|
it('can transfer child to another address', async function () {
|
||
|
const toOwnerAddress = addrs[2].address;
|
||
|
await expect(
|
||
|
parent
|
||
|
.connect(tokenOwner)
|
||
|
.transferChild(parentId, toOwnerAddress, 0, 0, child.address, childId1, false, '0x'),
|
||
|
)
|
||
|
.to.emit(parent, 'ChildTransferred')
|
||
|
.withArgs(parentId, 0, child.address, childId1, false);
|
||
|
|
||
|
await checkChildMovedToRootOwner(toOwnerAddress);
|
||
|
});
|
||
|
|
||
|
it('can transfer child to another NFT', async function () {
|
||
|
const newOwnerAddress = addrs[2].address;
|
||
|
const newParentId = 2;
|
||
|
await parent.mint(newOwnerAddress, newParentId);
|
||
|
await expect(
|
||
|
parent
|
||
|
.connect(tokenOwner)
|
||
|
.transferChild(
|
||
|
parentId,
|
||
|
parent.address,
|
||
|
newParentId,
|
||
|
0,
|
||
|
child.address,
|
||
|
childId1,
|
||
|
false,
|
||
|
'0x',
|
||
|
),
|
||
|
)
|
||
|
.to.emit(parent, 'ChildTransferred')
|
||
|
.withArgs(parentId, 0, child.address, childId1, false);
|
||
|
|
||
|
expect(await child.ownerOf(childId1)).to.eql(newOwnerAddress);
|
||
|
expect(await child.directOwnerOf(childId1)).to.eql([parent.address, bn(newParentId), true]);
|
||
|
expect(await parent.pendingChildrenOf(newParentId)).to.eql([[bn(childId1), child.address]]);
|
||
|
});
|
||
|
|
||
|
it('cannot transfer child out of index', async function () {
|
||
|
const toOwnerAddress = addrs[2].address;
|
||
|
const badIndex = 2;
|
||
|
await expect(
|
||
|
parent
|
||
|
.connect(tokenOwner)
|
||
|
.transferChild(parentId, toOwnerAddress, 0, badIndex, child.address, childId1, false, '0x'),
|
||
|
).to.be.revertedWithCustomError(parent, 'ChildIndexOutOfRange');
|
||
|
});
|
||
|
|
||
|
it('cannot transfer child if address or id do not match', async function () {
|
||
|
const otherAddress = addrs[1].address;
|
||
|
const otherChildId = 9999;
|
||
|
const toOwnerAddress = addrs[2].address;
|
||
|
await expect(
|
||
|
parent
|
||
|
.connect(tokenOwner)
|
||
|
.transferChild(parentId, toOwnerAddress, 0, 0, otherAddress, childId1, false, '0x'),
|
||
|
).to.be.revertedWithCustomError(parent, 'UnexpectedChildId');
|
||
|
await expect(
|
||
|
parent
|
||
|
.connect(tokenOwner)
|
||
|
.transferChild(parentId, toOwnerAddress, 0, 0, child.address, otherChildId, false, '0x'),
|
||
|
).to.be.revertedWithCustomError(parent, 'UnexpectedChildId');
|
||
|
});
|
||
|
|
||
|
it('can transfer child if approved', async function () {
|
||
|
const transferer = addrs[1];
|
||
|
const toOwner = tokenOwner.address;
|
||
|
await parent.connect(tokenOwner).approve(transferer.address, parentId);
|
||
|
|
||
|
await parent
|
||
|
.connect(transferer)
|
||
|
.transferChild(parentId, toOwner, 0, 0, child.address, childId1, false, '0x');
|
||
|
await checkChildMovedToRootOwner();
|
||
|
});
|
||
|
|
||
|
it('can transfer child if approved for all', async function () {
|
||
|
const operator = addrs[2];
|
||
|
const toOwner = tokenOwner.address;
|
||
|
await parent.connect(tokenOwner).setApprovalForAll(operator.address, true);
|
||
|
|
||
|
await parent
|
||
|
.connect(operator)
|
||
|
.transferChild(parentId, toOwner, 0, 0, child.address, childId1, false, '0x');
|
||
|
await checkChildMovedToRootOwner();
|
||
|
});
|
||
|
|
||
|
it('can transfer child with grandchild and children are ok', async function () {
|
||
|
const toOwner = tokenOwner.address;
|
||
|
const grandchildId = 999;
|
||
|
await child.nestMint(child.address, grandchildId, childId1);
|
||
|
|
||
|
// Transfer child from parent.
|
||
|
await parent
|
||
|
.connect(tokenOwner)
|
||
|
.transferChild(parentId, toOwner, 0, 0, child.address, childId1, false, '0x');
|
||
|
|
||
|
// New owner of child
|
||
|
expect(await child.ownerOf(childId1)).to.eql(tokenOwner.address);
|
||
|
expect(await child.directOwnerOf(childId1)).to.eql([tokenOwner.address, bn(0), false]);
|
||
|
|
||
|
// Grandchild is still owned by child
|
||
|
expect(await child.ownerOf(grandchildId)).to.eql(tokenOwner.address);
|
||
|
expect(await child.directOwnerOf(grandchildId)).to.eql([child.address, bn(childId1), true]);
|
||
|
});
|
||
|
|
||
|
it('cannot transfer child if not child root owner', async function () {
|
||
|
const toOwner = tokenOwner.address;
|
||
|
const notOwner = addrs[3];
|
||
|
await expect(
|
||
|
parent.connect(notOwner).transferChild(parentId, toOwner, 0, 0, child.address, childId1, false, '0x'),
|
||
|
).to.be.revertedWithCustomError(child, 'ERC721NotApprovedOrOwner');
|
||
|
});
|
||
|
|
||
|
it('cannot transfer child from not existing parent', async function () {
|
||
|
const badChildId = 99;
|
||
|
const toOwner = tokenOwner.address;
|
||
|
await expect(
|
||
|
parent
|
||
|
.connect(tokenOwner)
|
||
|
.transferChild(badChildId, toOwner, 0, 0, child.address, childId1, false, '0x'),
|
||
|
).to.be.revertedWithCustomError(child, 'ERC721InvalidTokenId');
|
||
|
});
|
||
|
|
||
|
async function checkChildMovedToRootOwner(rootOwnerAddress?: string) {
|
||
|
if (rootOwnerAddress === undefined) {
|
||
|
rootOwnerAddress = tokenOwner.address;
|
||
|
}
|
||
|
expect(await child.ownerOf(childId1)).to.eql(rootOwnerAddress);
|
||
|
expect(await child.directOwnerOf(childId1)).to.eql([rootOwnerAddress, bn(0), false]);
|
||
|
|
||
|
// Transferring updates balances downstream
|
||
|
expect(await child.balanceOf(rootOwnerAddress)).to.equal(1);
|
||
|
expect(await parent.balanceOf(tokenOwner.address)).to.equal(1);
|
||
|
}
|
||
|
});
|
||
|
|
||
|
describe('Transferring Pending Children', async function () {
|
||
|
let parentId: number;
|
||
|
let childId1: number;
|
||
|
|
||
|
beforeEach(async function () {
|
||
|
parentId = 1;
|
||
|
await parent.mint(tokenOwner.address, parentId);
|
||
|
childId1 = 99;
|
||
|
await child.nestMint(parent.address, childId1, parentId);
|
||
|
});
|
||
|
|
||
|
it('can transfer child with to as root owner', async function () {
|
||
|
await expect(
|
||
|
parent
|
||
|
.connect(tokenOwner)
|
||
|
.transferChild(parentId, tokenOwner.address, 0, 0, child.address, childId1, true, '0x'),
|
||
|
)
|
||
|
.to.emit(parent, 'ChildTransferred')
|
||
|
.withArgs(parentId, 0, child.address, childId1, true);
|
||
|
|
||
|
await checkChildMovedToRootOwner();
|
||
|
});
|
||
|
|
||
|
it('can transfer child to another address', async function () {
|
||
|
const toOwnerAddress = addrs[2].address;
|
||
|
await expect(
|
||
|
parent
|
||
|
.connect(tokenOwner)
|
||
|
.transferChild(parentId, toOwnerAddress, 0, 0, child.address, childId1, true, '0x'),
|
||
|
)
|
||
|
.to.emit(parent, 'ChildTransferred')
|
||
|
.withArgs(parentId, 0, child.address, childId1, true);
|
||
|
|
||
|
await checkChildMovedToRootOwner(toOwnerAddress);
|
||
|
});
|
||
|
|
||
|
it('can transfer child to another NFT', async function () {
|
||
|
const newOwnerAddress = addrs[2].address;
|
||
|
const newParentId = 2;
|
||
|
await parent.mint(newOwnerAddress, newParentId);
|
||
|
await expect(
|
||
|
parent
|
||
|
.connect(tokenOwner)
|
||
|
.transferChild(
|
||
|
parentId,
|
||
|
parent.address,
|
||
|
newParentId,
|
||
|
0,
|
||
|
child.address,
|
||
|
childId1,
|
||
|
true,
|
||
|
'0x',
|
||
|
),
|
||
|
)
|
||
|
.to.emit(parent, 'ChildTransferred')
|
||
|
.withArgs(parentId, 0, child.address, childId1, true);
|
||
|
|
||
|
expect(await child.ownerOf(childId1)).to.eql(newOwnerAddress);
|
||
|
expect(await child.directOwnerOf(childId1)).to.eql([parent.address, bn(newParentId), true]);
|
||
|
expect(await parent.pendingChildrenOf(newParentId)).to.eql([[bn(childId1), child.address]]);
|
||
|
});
|
||
|
|
||
|
it('cannot transfer child out of index', async function () {
|
||
|
const toOwnerAddress = addrs[2].address;
|
||
|
const badIndex = 2;
|
||
|
await expect(
|
||
|
parent
|
||
|
.connect(tokenOwner)
|
||
|
.transferChild(parentId, toOwnerAddress, 0, badIndex, child.address, childId1, true, '0x'),
|
||
|
).to.be.revertedWithCustomError(parent, 'PendingChildIndexOutOfRange');
|
||
|
});
|
||
|
|
||
|
it('cannot transfer child if address or id do not match', async function () {
|
||
|
const otherAddress = addrs[1].address;
|
||
|
const otherChildId = 9999;
|
||
|
const toOwnerAddress = addrs[2].address;
|
||
|
await expect(
|
||
|
parent
|
||
|
.connect(tokenOwner)
|
||
|
.transferChild(parentId, toOwnerAddress, 0, 0, otherAddress, childId1, true, '0x'),
|
||
|
).to.be.revertedWithCustomError(parent, 'UnexpectedChildId');
|
||
|
await expect(
|
||
|
parent
|
||
|
.connect(tokenOwner)
|
||
|
.transferChild(parentId, toOwnerAddress, 0, 0, child.address, otherChildId, true, '0x'),
|
||
|
).to.be.revertedWithCustomError(parent, 'UnexpectedChildId');
|
||
|
});
|
||
|
|
||
|
it('can transfer child if approved', async function () {
|
||
|
const transferer = addrs[1];
|
||
|
const toOwner = tokenOwner.address;
|
||
|
await parent.connect(tokenOwner).approve(transferer.address, parentId);
|
||
|
|
||
|
await parent
|
||
|
.connect(transferer)
|
||
|
.transferChild(parentId, toOwner, 0, 0,child.address, childId1, true, '0x');
|
||
|
await checkChildMovedToRootOwner();
|
||
|
});
|
||
|
|
||
|
it('can transfer child if approved for all', async function () {
|
||
|
const operator = addrs[2];
|
||
|
const toOwner = tokenOwner.address;
|
||
|
await parent.connect(tokenOwner).setApprovalForAll(operator.address, true);
|
||
|
|
||
|
await parent
|
||
|
.connect(operator)
|
||
|
.transferChild(parentId, toOwner, 0, 0, child.address, childId1, true, '0x');
|
||
|
await checkChildMovedToRootOwner();
|
||
|
});
|
||
|
|
||
|
it('can transfer child with grandchild and children are ok', async function () {
|
||
|
const toOwner = tokenOwner.address;
|
||
|
const grandchildId = 999;
|
||
|
await child.nestMint(child.address, grandchildId, childId1);
|
||
|
|
||
|
// Transfer child from parent.
|
||
|
await parent
|
||
|
.connect(tokenOwner)
|
||
|
.transferChild(parentId, toOwner, 0, 0, child.address, childId1, true, '0x');
|
||
|
|
||
|
// New owner of child
|
||
|
expect(await child.ownerOf(childId1)).to.eql(tokenOwner.address);
|
||
|
expect(await child.directOwnerOf(childId1)).to.eql([tokenOwner.address, bn(0), false]);
|
||
|
|
||
|
// Grandchild is still owned by child
|
||
|
expect(await child.ownerOf(grandchildId)).to.eql(tokenOwner.address);
|
||
|
expect(await child.directOwnerOf(grandchildId)).to.eql([child.address, bn(childId1), true]);
|
||
|
});
|
||
|
|
||
|
it('cannot transfer child if not child root owner', async function () {
|
||
|
const toOwner = tokenOwner.address;
|
||
|
const notOwner = addrs[3];
|
||
|
await expect(
|
||
|
parent.connect(notOwner).transferChild(parentId, toOwner, 0, 0, child.address, childId1, true, '0x'),
|
||
|
).to.be.revertedWithCustomError(child, 'ERC721NotApprovedOrOwner');
|
||
|
});
|
||
|
|
||
|
it('cannot transfer child from not existing parent', async function () {
|
||
|
const badChildId = 99;
|
||
|
const toOwner = tokenOwner.address;
|
||
|
await expect(
|
||
|
parent
|
||
|
.connect(tokenOwner)
|
||
|
.transferChild(badChildId, toOwner, 0, 0, child.address, childId1, true, '0x'),
|
||
|
).to.be.revertedWithCustomError(child, 'ERC721InvalidTokenId');
|
||
|
});
|
||
|
|
||
|
async function checkChildMovedToRootOwner(rootOwnerAddress?: string) {
|
||
|
if (rootOwnerAddress === undefined) {
|
||
|
rootOwnerAddress = tokenOwner.address;
|
||
|
}
|
||
|
expect(await child.ownerOf(childId1)).to.eql(rootOwnerAddress);
|
||
|
expect(await child.directOwnerOf(childId1)).to.eql([rootOwnerAddress, bn(0), false]);
|
||
|
|
||
|
// Transferring updates balances downstream
|
||
|
expect(await child.balanceOf(rootOwnerAddress)).to.equal(1);
|
||
|
expect(await parent.balanceOf(tokenOwner.address)).to.equal(1);
|
||
|
}
|
||
|
});
|
||
|
|
||
|
describe('Transfer', async function () {
|
||
|
it('can transfer token', async function () {
|
||
|
const firstOwner = addrs[1];
|
||
|
const newOwner = addrs[2];
|
||
|
const tokenId1 = 1;
|
||
|
await parent.mint(firstOwner.address, tokenId1);
|
||
|
await parent.connect(firstOwner).transfer(newOwner.address, tokenId1);
|
||
|
|
||
|
// Balances and ownership are updated
|
||
|
expect(await parent.ownerOf(tokenId1)).to.eql(newOwner.address);
|
||
|
expect(await parent.balanceOf(firstOwner.address)).to.equal(0);
|
||
|
expect(await parent.balanceOf(newOwner.address)).to.equal(1);
|
||
|
});
|
||
|
|
||
|
it('cannot transfer not owned token', async function () {
|
||
|
const firstOwner = addrs[1];
|
||
|
const newOwner = addrs[2];
|
||
|
const tokenId1 = 1;
|
||
|
await parent.mint(firstOwner.address, tokenId1);
|
||
|
await expect(
|
||
|
parent.connect(newOwner).transfer(newOwner.address, tokenId1),
|
||
|
).to.be.revertedWithCustomError(child, 'NotApprovedOrDirectOwner');
|
||
|
});
|
||
|
|
||
|
it('cannot transfer to address zero', async function () {
|
||
|
const firstOwner = addrs[1];
|
||
|
const tokenId1 = 1;
|
||
|
await parent.mint(firstOwner.address, tokenId1);
|
||
|
await expect(
|
||
|
parent.connect(firstOwner).transfer(ADDRESS_ZERO, tokenId1),
|
||
|
).to.be.revertedWithCustomError(child, 'ERC721TransferToTheZeroAddress');
|
||
|
});
|
||
|
|
||
|
it('can transfer token from approved address (not owner)', async function () {
|
||
|
const firstOwner = addrs[1];
|
||
|
const approved = addrs[2];
|
||
|
const newOwner = addrs[3];
|
||
|
const tokenId1 = 1;
|
||
|
await parent.mint(firstOwner.address, tokenId1);
|
||
|
|
||
|
await parent.connect(firstOwner).approve(approved.address, tokenId1);
|
||
|
await parent.connect(firstOwner).transfer(newOwner.address, tokenId1);
|
||
|
|
||
|
expect(await parent.ownerOf(tokenId1)).to.eql(newOwner.address);
|
||
|
});
|
||
|
|
||
|
it('can transfer not nested token with child to address and owners/children are ok', async function () {
|
||
|
const firstOwner = addrs[1];
|
||
|
const newOwner = addrs[2];
|
||
|
const parentId = 1;
|
||
|
await parent.mint(firstOwner.address, parentId);
|
||
|
const childId1 = 99;
|
||
|
await child.nestMint(parent.address, childId1, parentId);
|
||
|
|
||
|
await parent.connect(firstOwner).transfer(newOwner.address, parentId);
|
||
|
|
||
|
// Balances and ownership are updated
|
||
|
expect(await parent.balanceOf(firstOwner.address)).to.equal(0);
|
||
|
expect(await parent.balanceOf(newOwner.address)).to.equal(1);
|
||
|
|
||
|
expect(await parent.ownerOf(parentId)).to.eql(newOwner.address);
|
||
|
expect(await parent.directOwnerOf(parentId)).to.eql([newOwner.address, bn(0), false]);
|
||
|
|
||
|
// New owner of child
|
||
|
expect(await child.ownerOf(childId1)).to.eql(newOwner.address);
|
||
|
expect(await child.directOwnerOf(childId1)).to.eql([parent.address, bn(parentId), true]);
|
||
|
|
||
|
// Parent still has its children
|
||
|
expect(await parent.pendingChildrenOf(parentId)).to.eql([[bn(childId1), child.address]]);
|
||
|
});
|
||
|
|
||
|
it('cannot directly transfer nested child', async function () {
|
||
|
const firstOwner = addrs[1];
|
||
|
const newOwner = addrs[2];
|
||
|
const parentId = 1;
|
||
|
await parent.mint(firstOwner.address, parentId);
|
||
|
const childId1 = 99;
|
||
|
await child.nestMint(parent.address, childId1, parentId);
|
||
|
|
||
|
await expect(
|
||
|
child.connect(firstOwner).transfer(newOwner.address, childId1),
|
||
|
).to.be.revertedWithCustomError(child, 'NotApprovedOrDirectOwner');
|
||
|
});
|
||
|
|
||
|
it('can transfer parent token to token with same owner, family tree is ok', async function () {
|
||
|
const firstOwner = addrs[1];
|
||
|
const grandParentId = 999;
|
||
|
await parent.mint(firstOwner.address, grandParentId);
|
||
|
const parentId = 1;
|
||
|
await parent.mint(firstOwner.address, parentId);
|
||
|
const childId1 = 99;
|
||
|
await child.nestMint(parent.address, childId1, parentId);
|
||
|
|
||
|
// Check balances
|
||
|
expect(await parent.balanceOf(firstOwner.address)).to.equal(2);
|
||
|
expect(await child.balanceOf(parent.address)).to.equal(1);
|
||
|
|
||
|
// Transfers token parentId to (parent.address, token grandParentId)
|
||
|
await parent.connect(firstOwner).nestTransfer(parent.address, parentId, grandParentId);
|
||
|
|
||
|
// Balances unchanged since root owner is the same
|
||
|
expect(await parent.balanceOf(firstOwner.address)).to.equal(1);
|
||
|
expect(await child.balanceOf(parent.address)).to.equal(1);
|
||
|
expect(await parent.balanceOf(parent.address)).to.equal(1);
|
||
|
|
||
|
// Parent is still owner of child
|
||
|
let expected = [bn(childId1), child.address];
|
||
|
checkAcceptedAndPendingChildren(parent, parentId, [expected], []);
|
||
|
// Ownership: firstOwner > newGrandparent > parent > child
|
||
|
expected = [bn(parentId), parent.address];
|
||
|
checkAcceptedAndPendingChildren(parent, grandParentId, [], [expected]);
|
||
|
});
|
||
|
|
||
|
it('can transfer parent token to token with different owner, family tree is ok', async function () {
|
||
|
const firstOwner = addrs[1];
|
||
|
const otherOwner = addrs[2];
|
||
|
const grandParentId = 999;
|
||
|
await parent.mint(otherOwner.address, grandParentId);
|
||
|
const parentId = 1;
|
||
|
await parent.mint(firstOwner.address, parentId);
|
||
|
const childId1 = 99;
|
||
|
await child.nestMint(parent.address, childId1, parentId);
|
||
|
|
||
|
// Check balances
|
||
|
expect(await parent.balanceOf(otherOwner.address)).to.equal(1);
|
||
|
expect(await parent.balanceOf(firstOwner.address)).to.equal(1);
|
||
|
expect(await child.balanceOf(parent.address)).to.equal(1);
|
||
|
|
||
|
// firstOwner calls parent to transfer parent token parent
|
||
|
await parent.connect(firstOwner).nestTransfer(parent.address, parentId, grandParentId);
|
||
|
|
||
|
// Balances update
|
||
|
expect(await parent.balanceOf(firstOwner.address)).to.equal(0);
|
||
|
expect(await parent.balanceOf(parent.address)).to.equal(1);
|
||
|
expect(await parent.balanceOf(otherOwner.address)).to.equal(1);
|
||
|
expect(await child.balanceOf(parent.address)).to.equal(1);
|
||
|
|
||
|
// Parent is still owner of child
|
||
|
let expected = [bn(childId1), child.address];
|
||
|
checkAcceptedAndPendingChildren(parent, parentId, [expected], []);
|
||
|
// Ownership: firstOwner > newGrandparent > parent > child
|
||
|
expected = [bn(parentId), parent.address];
|
||
|
checkAcceptedAndPendingChildren(parent, grandParentId, [], [expected]);
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('Nest Transfer', async function () {
|
||
|
let firstOwner: SignerWithAddress;
|
||
|
let parentId: number;
|
||
|
let childId1: number;
|
||
|
|
||
|
beforeEach(async function () {
|
||
|
firstOwner = addrs[1];
|
||
|
parentId = 1;
|
||
|
childId1 = 99;
|
||
|
await parent.mint(firstOwner.address, parentId);
|
||
|
await child.mint(firstOwner.address, childId1);
|
||
|
});
|
||
|
|
||
|
it('cannot nest tranfer from non immediate owner (owner of parent)', async function () {
|
||
|
const otherParentId = 2;
|
||
|
await parent.mint(firstOwner.address, otherParentId);
|
||
|
// We send it to the parent first
|
||
|
await child.connect(firstOwner).nestTransfer(parent.address, childId1, parentId);
|
||
|
// We can no longer nest transfer it, even if we are the root owner:
|
||
|
await expect(
|
||
|
child.connect(firstOwner).nestTransfer(parent.address, childId1, otherParentId),
|
||
|
).to.be.revertedWithCustomError(child, 'NotApprovedOrDirectOwner');
|
||
|
});
|
||
|
|
||
|
it('cannot nest tranfer to same NFT', async function () {
|
||
|
// We can no longer nest transfer it, even if we are the root owner:
|
||
|
await expect(
|
||
|
child.connect(firstOwner).nestTransfer(child.address, childId1, childId1),
|
||
|
).to.be.revertedWithCustomError(child, 'NestableTransferToSelf');
|
||
|
});
|
||
|
|
||
|
it('cannot nest tranfer a descendant same NFT', async function () {
|
||
|
// We can no longer nest transfer it, even if we are the root owner:
|
||
|
await child.connect(firstOwner).nestTransfer(parent.address, childId1, parentId);
|
||
|
const grandChildId = 999;
|
||
|
await child.nestMint(child.address, grandChildId, childId1);
|
||
|
// Ownership is now parent->child->granChild
|
||
|
// Cannot send parent to grandChild
|
||
|
await expect(
|
||
|
parent.connect(firstOwner).nestTransfer(child.address, parentId, grandChildId),
|
||
|
).to.be.revertedWithCustomError(child, 'NestableTransferToDescendant');
|
||
|
// Cannot send parent to child
|
||
|
await expect(
|
||
|
parent.connect(firstOwner).nestTransfer(child.address, parentId, childId1),
|
||
|
).to.be.revertedWithCustomError(child, 'NestableTransferToDescendant');
|
||
|
});
|
||
|
|
||
|
it('cannot nest tranfer if ancestors tree is too deep', async function () {
|
||
|
let lastId = childId1;
|
||
|
for (let i = 101; i <= 200; i++) {
|
||
|
await child.nestMint(child.address, i, lastId);
|
||
|
lastId = i;
|
||
|
}
|
||
|
// Ownership is now parent->child->child->child->child...->lastChild
|
||
|
// Cannot send parent to lastChild
|
||
|
await expect(
|
||
|
parent.connect(firstOwner).nestTransfer(child.address, parentId, lastId),
|
||
|
).to.be.revertedWithCustomError(child, 'NestableTooDeep');
|
||
|
});
|
||
|
|
||
|
it('cannot nest tranfer if not owner', async function () {
|
||
|
const notOwner = addrs[3];
|
||
|
await expect(
|
||
|
child.connect(notOwner).nestTransfer(parent.address, childId1, parentId),
|
||
|
).to.be.revertedWithCustomError(child, 'NotApprovedOrDirectOwner');
|
||
|
});
|
||
|
|
||
|
it('cannot nest tranfer to address 0', async function () {
|
||
|
await expect(
|
||
|
child.connect(firstOwner).nestTransfer(ADDRESS_ZERO, childId1, parentId),
|
||
|
).to.be.revertedWithCustomError(child, 'ERC721TransferToTheZeroAddress');
|
||
|
});
|
||
|
|
||
|
it('cannot nest tranfer to a non contract', async function () {
|
||
|
const newOwner = addrs[2];
|
||
|
await expect(
|
||
|
child.connect(firstOwner).nestTransfer(newOwner.address, childId1, parentId),
|
||
|
).to.be.revertedWithCustomError(child, 'IsNotContract');
|
||
|
});
|
||
|
|
||
|
it('cannot nest tranfer to contract if it does implement IERC6059', async function () {
|
||
|
const ERC721 = await ethers.getContractFactory('ERC721Mock');
|
||
|
const nonNestable = await ERC721.deploy('Non receiver', 'NR');
|
||
|
await nonNestable.deployed();
|
||
|
await expect(
|
||
|
child.connect(firstOwner).nestTransfer(nonNestable.address, childId1, parentId),
|
||
|
).to.be.revertedWithCustomError(child, 'NestableTransferToNonNestableImplementer');
|
||
|
});
|
||
|
|
||
|
it('can nest tranfer to IERC6059 contract', async function () {
|
||
|
await child.connect(firstOwner).nestTransfer(parent.address, childId1, parentId);
|
||
|
expect(await child.ownerOf(childId1)).to.eql(firstOwner.address);
|
||
|
expect(await child.directOwnerOf(childId1)).to.eql([parent.address, bn(parentId), true]);
|
||
|
});
|
||
|
|
||
|
it('cannot nest tranfer to non existing parent token', async function () {
|
||
|
const notExistingParentId = 9999;
|
||
|
await expect(
|
||
|
child.connect(firstOwner).nestTransfer(parent.address, childId1, notExistingParentId),
|
||
|
).to.be.revertedWithCustomError(parent, 'ERC721InvalidTokenId');
|
||
|
});
|
||
|
});
|
||
|
|
||
|
async function checkNoChildrenNorPending(parentId: number): Promise<void> {
|
||
|
expect(await parent.pendingChildrenOf(parentId)).to.eql([]);
|
||
|
expect(await parent.childrenOf(parentId)).to.eql([]);
|
||
|
}
|
||
|
|
||
|
async function checkAcceptedAndPendingChildren(
|
||
|
contract: NestableTokenMock,
|
||
|
tokenId1: number,
|
||
|
expectedAccepted: any[],
|
||
|
expectedPending: any[],
|
||
|
) {
|
||
|
const accepted = await contract.childrenOf(tokenId1);
|
||
|
expect(accepted).to.eql(expectedAccepted);
|
||
|
|
||
|
const pending = await contract.pendingChildrenOf(tokenId1);
|
||
|
expect(pending).to.eql(expectedPending);
|
||
|
}
|
||
|
});
|