forked from DecentralizedClimateFoundation/DCIPs
1494 lines
48 KiB
TypeScript
1494 lines
48 KiB
TypeScript
|
import { loadFixture } from "@nomicfoundation/hardhat-network-helpers";
|
||
|
import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers";
|
||
|
import { expect } from "chai";
|
||
|
import { BigNumber, constants } from "ethers";
|
||
|
import { ethers } from "hardhat";
|
||
|
import { EquippableTokenMock } from "../typechain-types";
|
||
|
|
||
|
function bn(x: number): BigNumber {
|
||
|
return BigNumber.from(x);
|
||
|
}
|
||
|
|
||
|
const ADDRESS_ZERO = constants.AddressZero;
|
||
|
|
||
|
async function parentChildFixture(): Promise<{
|
||
|
parent: EquippableTokenMock;
|
||
|
child: EquippableTokenMock;
|
||
|
}> {
|
||
|
const factory = await ethers.getContractFactory("EquippableTokenMock");
|
||
|
|
||
|
const parent = await factory.deploy();
|
||
|
await parent.deployed();
|
||
|
const child = await factory.deploy();
|
||
|
await child.deployed();
|
||
|
return { parent, child };
|
||
|
}
|
||
|
|
||
|
describe("NestableToken", function () {
|
||
|
let parent: EquippableTokenMock;
|
||
|
let child: EquippableTokenMock;
|
||
|
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 INestable", 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 INestable", 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 INestable 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: EquippableTokenMock,
|
||
|
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);
|
||
|
}
|
||
|
});
|