ERC6909.behavior.js 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  1. const { ethers } = require('hardhat');
  2. const { expect } = require('chai');
  3. const { shouldSupportInterfaces } = require('../../utils/introspection/SupportsInterface.behavior');
  4. function shouldBehaveLikeERC6909() {
  5. const firstTokenId = 1n;
  6. const secondTokenId = 2n;
  7. const randomTokenId = 125523n;
  8. const firstTokenSupply = 2000n;
  9. const secondTokenSupply = 3000n;
  10. const amount = 100n;
  11. describe('like an ERC6909', function () {
  12. describe('balanceOf', function () {
  13. describe("when accounts don't own tokens", function () {
  14. it('return zero', async function () {
  15. await expect(this.token.balanceOf(this.holder, firstTokenId)).to.eventually.be.equal(0n);
  16. await expect(this.token.balanceOf(this.holder, secondTokenId)).to.eventually.be.equal(0n);
  17. await expect(this.token.balanceOf(this.other, randomTokenId)).to.eventually.be.equal(0n);
  18. });
  19. });
  20. describe('when accounts own some tokens', function () {
  21. beforeEach(async function () {
  22. await this.token.$_mint(this.holder, firstTokenId, firstTokenSupply);
  23. await this.token.$_mint(this.holder, secondTokenId, secondTokenSupply);
  24. });
  25. it('returns amount owned by the given address', async function () {
  26. await expect(this.token.balanceOf(this.holder, firstTokenId)).to.eventually.be.equal(firstTokenSupply);
  27. await expect(this.token.balanceOf(this.holder, secondTokenId)).to.eventually.be.equal(secondTokenSupply);
  28. await expect(this.token.balanceOf(this.other, firstTokenId)).to.eventually.be.equal(0n);
  29. });
  30. });
  31. });
  32. describe('setOperator', function () {
  33. it('emits an OperatorSet event and updated the value', async function () {
  34. await expect(this.token.connect(this.holder).setOperator(this.operator, true))
  35. .to.emit(this.token, 'OperatorSet')
  36. .withArgs(this.holder, this.operator, true);
  37. // operator for holder
  38. await expect(this.token.isOperator(this.holder, this.operator)).to.eventually.be.true;
  39. // not operator for other account
  40. await expect(this.token.isOperator(this.other, this.operator)).to.eventually.be.false;
  41. });
  42. it('can unset the operator approval', async function () {
  43. await this.token.connect(this.holder).setOperator(this.operator, true);
  44. // before
  45. await expect(this.token.isOperator(this.holder, this.operator)).to.eventually.be.true;
  46. // unset
  47. await expect(this.token.connect(this.holder).setOperator(this.operator, false))
  48. .to.emit(this.token, 'OperatorSet')
  49. .withArgs(this.holder, this.operator, false);
  50. // after
  51. await expect(this.token.isOperator(this.holder, this.operator)).to.eventually.be.false;
  52. });
  53. it('cannot set address(0) as an operator', async function () {
  54. await expect(this.token.connect(this.holder).setOperator(ethers.ZeroAddress, true))
  55. .to.be.revertedWithCustomError(this.token, 'ERC6909InvalidSpender')
  56. .withArgs(ethers.ZeroAddress);
  57. });
  58. });
  59. describe('approve', function () {
  60. it('emits an Approval event and updates allowance', async function () {
  61. await expect(this.token.connect(this.holder).approve(this.operator, firstTokenId, firstTokenSupply))
  62. .to.emit(this.token, 'Approval')
  63. .withArgs(this.holder, this.operator, firstTokenId, firstTokenSupply);
  64. // approved
  65. await expect(this.token.allowance(this.holder, this.operator, firstTokenId)).to.eventually.be.equal(
  66. firstTokenSupply,
  67. );
  68. // other account is not approved
  69. await expect(this.token.allowance(this.other, this.operator, firstTokenId)).to.eventually.be.equal(0n);
  70. });
  71. it('can unset the approval', async function () {
  72. await expect(this.token.connect(this.holder).approve(this.operator, firstTokenId, 0n))
  73. .to.emit(this.token, 'Approval')
  74. .withArgs(this.holder, this.operator, firstTokenId, 0n);
  75. await expect(this.token.allowance(this.holder, this.operator, firstTokenId)).to.eventually.be.equal(0n);
  76. });
  77. it('cannot give allowance to address(0)', async function () {
  78. await expect(this.token.connect(this.holder).approve(ethers.ZeroAddress, firstTokenId, firstTokenSupply))
  79. .to.be.revertedWithCustomError(this.token, 'ERC6909InvalidSpender')
  80. .withArgs(ethers.ZeroAddress);
  81. });
  82. });
  83. describe('transfer', function () {
  84. beforeEach(async function () {
  85. await this.token.$_mint(this.holder, firstTokenId, firstTokenSupply);
  86. await this.token.$_mint(this.holder, secondTokenId, secondTokenSupply);
  87. });
  88. it('transfers to the zero address are blocked', async function () {
  89. await expect(this.token.connect(this.holder).transfer(ethers.ZeroAddress, firstTokenId, firstTokenSupply))
  90. .to.be.revertedWithCustomError(this.token, 'ERC6909InvalidReceiver')
  91. .withArgs(ethers.ZeroAddress);
  92. });
  93. it('reverts when insufficient balance', async function () {
  94. await expect(this.token.connect(this.holder).transfer(this.recipient, firstTokenId, firstTokenSupply + 1n))
  95. .to.be.revertedWithCustomError(this.token, 'ERC6909InsufficientBalance')
  96. .withArgs(this.holder, firstTokenSupply, firstTokenSupply + 1n, firstTokenId);
  97. });
  98. it('emits event and transfers tokens', async function () {
  99. await expect(this.token.connect(this.holder).transfer(this.recipient, firstTokenId, amount))
  100. .to.emit(this.token, 'Transfer')
  101. .withArgs(this.holder, this.holder, this.recipient, firstTokenId, amount);
  102. await expect(this.token.balanceOf(this.holder, firstTokenId)).to.eventually.equal(firstTokenSupply - amount);
  103. await expect(this.token.balanceOf(this.recipient, firstTokenId)).to.eventually.equal(amount);
  104. });
  105. });
  106. describe('transferFrom', function () {
  107. beforeEach(async function () {
  108. await this.token.$_mint(this.holder, firstTokenId, firstTokenSupply);
  109. await this.token.$_mint(this.holder, secondTokenId, secondTokenSupply);
  110. });
  111. it('transfer from self', async function () {
  112. await expect(this.token.connect(this.holder).transferFrom(this.holder, this.recipient, firstTokenId, amount))
  113. .to.emit(this.token, 'Transfer')
  114. .withArgs(this.holder, this.holder, this.recipient, firstTokenId, amount);
  115. await expect(this.token.balanceOf(this.holder, firstTokenId)).to.eventually.equal(firstTokenSupply - amount);
  116. await expect(this.token.balanceOf(this.recipient, firstTokenId)).to.eventually.equal(amount);
  117. });
  118. describe('with approval', async function () {
  119. beforeEach(async function () {
  120. await this.token.connect(this.holder).approve(this.operator, firstTokenId, amount);
  121. });
  122. it('reverts when insufficient allowance', async function () {
  123. await expect(
  124. this.token.connect(this.operator).transferFrom(this.holder, this.recipient, firstTokenId, amount + 1n),
  125. )
  126. .to.be.revertedWithCustomError(this.token, 'ERC6909InsufficientAllowance')
  127. .withArgs(this.operator, amount, amount + 1n, firstTokenId);
  128. });
  129. it('should emit transfer event and update approval (without an Approval event)', async function () {
  130. await expect(
  131. this.token.connect(this.operator).transferFrom(this.holder, this.recipient, firstTokenId, amount - 1n),
  132. )
  133. .to.emit(this.token, 'Transfer')
  134. .withArgs(this.operator, this.holder, this.recipient, firstTokenId, amount - 1n)
  135. .to.not.emit(this.token, 'Approval');
  136. await expect(this.token.allowance(this.holder, this.operator, firstTokenId)).to.eventually.equal(1n);
  137. });
  138. it("shouldn't reduce allowance when infinite", async function () {
  139. await this.token.connect(this.holder).approve(this.operator, firstTokenId, ethers.MaxUint256);
  140. await this.token.connect(this.operator).transferFrom(this.holder, this.recipient, firstTokenId, amount);
  141. await expect(this.token.allowance(this.holder, this.operator, firstTokenId)).to.eventually.equal(
  142. ethers.MaxUint256,
  143. );
  144. });
  145. });
  146. });
  147. describe('with operator approval', function () {
  148. beforeEach(async function () {
  149. await this.token.connect(this.holder).setOperator(this.operator, true);
  150. await this.token.$_mint(this.holder, firstTokenId, firstTokenSupply);
  151. });
  152. it('operator can transfer', async function () {
  153. await expect(this.token.connect(this.operator).transferFrom(this.holder, this.recipient, firstTokenId, amount))
  154. .to.emit(this.token, 'Transfer')
  155. .withArgs(this.operator, this.holder, this.recipient, firstTokenId, amount);
  156. await expect(this.token.balanceOf(this.holder, firstTokenId)).to.eventually.equal(firstTokenSupply - amount);
  157. await expect(this.token.balanceOf(this.recipient, firstTokenId)).to.eventually.equal(amount);
  158. });
  159. it('operator transfer does not reduce allowance', async function () {
  160. // Also give allowance
  161. await this.token.connect(this.holder).approve(this.operator, firstTokenId, firstTokenSupply);
  162. await expect(this.token.connect(this.operator).transferFrom(this.holder, this.recipient, firstTokenId, amount))
  163. .to.emit(this.token, 'Transfer')
  164. .withArgs(this.operator, this.holder, this.recipient, firstTokenId, amount);
  165. await expect(this.token.allowance(this.holder, this.operator, firstTokenId)).to.eventually.equal(
  166. firstTokenSupply,
  167. );
  168. });
  169. });
  170. shouldSupportInterfaces(['ERC6909']);
  171. });
  172. }
  173. module.exports = {
  174. shouldBehaveLikeERC6909,
  175. };