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 { 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); } });