ERC721Consecutive.test.js 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228
  1. const { ethers } = require('hardhat');
  2. const { expect } = require('chai');
  3. const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
  4. const { sum } = require('../../../helpers/math');
  5. const name = 'Non Fungible Token';
  6. const symbol = 'NFT';
  7. describe('ERC721Consecutive', function () {
  8. for (const offset of [0n, 1n, 42n]) {
  9. describe(`with offset ${offset}`, function () {
  10. async function fixture() {
  11. const accounts = await ethers.getSigners();
  12. const [alice, bruce, chris, receiver] = accounts;
  13. const batches = [
  14. { receiver: alice, amount: 0n },
  15. { receiver: alice, amount: 1n },
  16. { receiver: alice, amount: 2n },
  17. { receiver: bruce, amount: 5n },
  18. { receiver: chris, amount: 0n },
  19. { receiver: alice, amount: 7n },
  20. ];
  21. const delegates = [alice, chris];
  22. const token = await ethers.deployContract('$ERC721ConsecutiveMock', [
  23. name,
  24. symbol,
  25. offset,
  26. delegates,
  27. batches.map(({ receiver }) => receiver),
  28. batches.map(({ amount }) => amount),
  29. ]);
  30. return { accounts, alice, bruce, chris, receiver, batches, delegates, token };
  31. }
  32. beforeEach(async function () {
  33. Object.assign(this, await loadFixture(fixture));
  34. });
  35. describe('minting during construction', function () {
  36. it('events are emitted at construction', async function () {
  37. let first = offset;
  38. for (const batch of this.batches) {
  39. if (batch.amount > 0) {
  40. await expect(this.token.deploymentTransaction())
  41. .to.emit(this.token, 'ConsecutiveTransfer')
  42. .withArgs(
  43. first /* fromTokenId */,
  44. first + batch.amount - 1n /* toTokenId */,
  45. ethers.ZeroAddress /* fromAddress */,
  46. batch.receiver /* toAddress */,
  47. );
  48. } else {
  49. // ".to.not.emit" only looks at event name, and doesn't check the parameters
  50. }
  51. first += batch.amount;
  52. }
  53. });
  54. it('ownership is set', async function () {
  55. const owners = [
  56. ...Array(Number(offset)).fill(ethers.ZeroAddress),
  57. ...this.batches.flatMap(({ receiver, amount }) => Array(Number(amount)).fill(receiver.address)),
  58. ];
  59. for (const tokenId in owners) {
  60. if (owners[tokenId] != ethers.ZeroAddress) {
  61. expect(await this.token.ownerOf(tokenId)).to.equal(owners[tokenId]);
  62. }
  63. }
  64. });
  65. it('balance & voting power are set', async function () {
  66. for (const account of this.accounts) {
  67. const balance =
  68. sum(...this.batches.filter(({ receiver }) => receiver === account).map(({ amount }) => amount)) ?? 0n;
  69. expect(await this.token.balanceOf(account)).to.equal(balance);
  70. // If not delegated at construction, check before + do delegation
  71. if (!this.delegates.includes(account)) {
  72. expect(await this.token.getVotes(account)).to.equal(0n);
  73. await this.token.connect(account).delegate(account);
  74. }
  75. // At this point all accounts should have delegated
  76. expect(await this.token.getVotes(account)).to.equal(balance);
  77. }
  78. });
  79. it('reverts on consecutive minting to the zero address', async function () {
  80. await expect(
  81. ethers.deployContract('$ERC721ConsecutiveMock', [
  82. name,
  83. symbol,
  84. offset,
  85. this.delegates,
  86. [ethers.ZeroAddress],
  87. [10],
  88. ]),
  89. )
  90. .to.be.revertedWithCustomError(this.token, 'ERC721InvalidReceiver')
  91. .withArgs(ethers.ZeroAddress);
  92. });
  93. });
  94. describe('minting after construction', function () {
  95. it('consecutive minting is not possible after construction', async function () {
  96. await expect(this.token.$_mintConsecutive(this.alice, 10)).to.be.revertedWithCustomError(
  97. this.token,
  98. 'ERC721ForbiddenBatchMint',
  99. );
  100. });
  101. it('simple minting is possible after construction', async function () {
  102. const tokenId = sum(...this.batches.map(b => b.amount)) + offset;
  103. await expect(this.token.ownerOf(tokenId))
  104. .to.be.revertedWithCustomError(this.token, 'ERC721NonexistentToken')
  105. .withArgs(tokenId);
  106. await expect(this.token.$_mint(this.alice, tokenId))
  107. .to.emit(this.token, 'Transfer')
  108. .withArgs(ethers.ZeroAddress, this.alice, tokenId);
  109. });
  110. it('cannot mint a token that has been batched minted', async function () {
  111. const tokenId = sum(...this.batches.map(b => b.amount)) + offset - 1n;
  112. expect(await this.token.ownerOf(tokenId)).to.not.equal(ethers.ZeroAddress);
  113. await expect(this.token.$_mint(this.alice, tokenId))
  114. .to.be.revertedWithCustomError(this.token, 'ERC721InvalidSender')
  115. .withArgs(ethers.ZeroAddress);
  116. });
  117. });
  118. describe('ERC721 behavior', function () {
  119. const tokenId = offset + 1n;
  120. it('core takes over ownership on transfer', async function () {
  121. await this.token.connect(this.alice).transferFrom(this.alice, this.receiver, tokenId);
  122. expect(await this.token.ownerOf(tokenId)).to.equal(this.receiver);
  123. });
  124. it('tokens can be burned and re-minted #1', async function () {
  125. await expect(this.token.connect(this.alice).$_burn(tokenId))
  126. .to.emit(this.token, 'Transfer')
  127. .withArgs(this.alice, ethers.ZeroAddress, tokenId);
  128. await expect(this.token.ownerOf(tokenId))
  129. .to.be.revertedWithCustomError(this.token, 'ERC721NonexistentToken')
  130. .withArgs(tokenId);
  131. await expect(this.token.$_mint(this.bruce, tokenId))
  132. .to.emit(this.token, 'Transfer')
  133. .withArgs(ethers.ZeroAddress, this.bruce, tokenId);
  134. expect(await this.token.ownerOf(tokenId)).to.equal(this.bruce);
  135. });
  136. it('tokens can be burned and re-minted #2', async function () {
  137. const tokenId = sum(...this.batches.map(({ amount }) => amount)) + offset;
  138. await expect(this.token.ownerOf(tokenId))
  139. .to.be.revertedWithCustomError(this.token, 'ERC721NonexistentToken')
  140. .withArgs(tokenId);
  141. // mint
  142. await expect(this.token.$_mint(this.alice, tokenId))
  143. .to.emit(this.token, 'Transfer')
  144. .withArgs(ethers.ZeroAddress, this.alice, tokenId);
  145. expect(await this.token.ownerOf(tokenId)).to.equal(this.alice);
  146. // burn
  147. await expect(await this.token.$_burn(tokenId))
  148. .to.emit(this.token, 'Transfer')
  149. .withArgs(this.alice, ethers.ZeroAddress, tokenId);
  150. await expect(this.token.ownerOf(tokenId))
  151. .to.be.revertedWithCustomError(this.token, 'ERC721NonexistentToken')
  152. .withArgs(tokenId);
  153. // re-mint
  154. await expect(this.token.$_mint(this.bruce, tokenId))
  155. .to.emit(this.token, 'Transfer')
  156. .withArgs(ethers.ZeroAddress, this.bruce, tokenId);
  157. expect(await this.token.ownerOf(tokenId)).to.equal(this.bruce);
  158. });
  159. });
  160. });
  161. }
  162. describe('invalid use', function () {
  163. const receiver = ethers.Wallet.createRandom();
  164. it('cannot mint a batch larger than 5000', async function () {
  165. const factory = await ethers.getContractFactory('$ERC721ConsecutiveMock');
  166. await expect(ethers.deployContract('$ERC721ConsecutiveMock', [name, symbol, 0, [], [receiver], [5001n]]))
  167. .to.be.revertedWithCustomError(factory, 'ERC721ExceededMaxBatchMint')
  168. .withArgs(5001n, 5000n);
  169. });
  170. it('cannot use single minting during construction', async function () {
  171. const factory = await ethers.getContractFactory('$ERC721ConsecutiveNoConstructorMintMock');
  172. await expect(
  173. ethers.deployContract('$ERC721ConsecutiveNoConstructorMintMock', [name, symbol]),
  174. ).to.be.revertedWithCustomError(factory, 'ERC721ForbiddenMint');
  175. });
  176. it('consecutive mint not compatible with enumerability', async function () {
  177. const factory = await ethers.getContractFactory('$ERC721ConsecutiveEnumerableMock');
  178. await expect(
  179. ethers.deployContract('$ERC721ConsecutiveEnumerableMock', [name, symbol, [receiver], [100n]]),
  180. ).to.be.revertedWithCustomError(factory, 'ERC721EnumerableForbiddenBatchMint');
  181. });
  182. });
  183. });