ERC20Wrapper.test.js 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201
  1. const { ethers } = require('hardhat');
  2. const { expect } = require('chai');
  3. const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
  4. const { shouldBehaveLikeERC20 } = require('../ERC20.behavior');
  5. const name = 'My Token';
  6. const symbol = 'MTKN';
  7. const decimals = 9n;
  8. const initialSupply = 100n;
  9. async function fixture() {
  10. const [initialHolder, recipient, anotherAccount] = await ethers.getSigners();
  11. const underlying = await ethers.deployContract('$ERC20DecimalsMock', [name, symbol, decimals]);
  12. await underlying.$_mint(initialHolder, initialSupply);
  13. const token = await ethers.deployContract('$ERC20Wrapper', [`Wrapped ${name}`, `W${symbol}`, underlying]);
  14. return { initialHolder, recipient, anotherAccount, underlying, token };
  15. }
  16. describe('ERC20Wrapper', function () {
  17. beforeEach(async function () {
  18. Object.assign(this, await loadFixture(fixture));
  19. });
  20. afterEach('Underlying balance', async function () {
  21. expect(await this.underlying.balanceOf(this.token)).to.equal(await this.token.totalSupply());
  22. });
  23. it('has a name', async function () {
  24. expect(await this.token.name()).to.equal(`Wrapped ${name}`);
  25. });
  26. it('has a symbol', async function () {
  27. expect(await this.token.symbol()).to.equal(`W${symbol}`);
  28. });
  29. it('has the same decimals as the underlying token', async function () {
  30. expect(await this.token.decimals()).to.equal(decimals);
  31. });
  32. it('decimals default back to 18 if token has no metadata', async function () {
  33. const noDecimals = await ethers.deployContract('CallReceiverMock');
  34. const token = await ethers.deployContract('$ERC20Wrapper', [`Wrapped ${name}`, `W${symbol}`, noDecimals]);
  35. expect(await token.decimals()).to.equal(18n);
  36. });
  37. it('has underlying', async function () {
  38. expect(await this.token.underlying()).to.equal(this.underlying);
  39. });
  40. describe('deposit', function () {
  41. it('executes with approval', async function () {
  42. await this.underlying.connect(this.initialHolder).approve(this.token, initialSupply);
  43. const tx = await this.token.connect(this.initialHolder).depositFor(this.initialHolder, initialSupply);
  44. await expect(tx)
  45. .to.emit(this.underlying, 'Transfer')
  46. .withArgs(this.initialHolder, this.token, initialSupply)
  47. .to.emit(this.token, 'Transfer')
  48. .withArgs(ethers.ZeroAddress, this.initialHolder, initialSupply);
  49. await expect(tx).to.changeTokenBalances(
  50. this.underlying,
  51. [this.initialHolder, this.token],
  52. [-initialSupply, initialSupply],
  53. );
  54. await expect(tx).to.changeTokenBalance(this.token, this.initialHolder, initialSupply);
  55. });
  56. it('reverts when missing approval', async function () {
  57. await expect(this.token.connect(this.initialHolder).depositFor(this.initialHolder, initialSupply))
  58. .to.be.revertedWithCustomError(this.underlying, 'ERC20InsufficientAllowance')
  59. .withArgs(this.token, 0, initialSupply);
  60. });
  61. it('reverts when inssuficient balance', async function () {
  62. await this.underlying.connect(this.initialHolder).approve(this.token, ethers.MaxUint256);
  63. await expect(this.token.connect(this.initialHolder).depositFor(this.initialHolder, ethers.MaxUint256))
  64. .to.be.revertedWithCustomError(this.underlying, 'ERC20InsufficientBalance')
  65. .withArgs(this.initialHolder, initialSupply, ethers.MaxUint256);
  66. });
  67. it('deposits to other account', async function () {
  68. await this.underlying.connect(this.initialHolder).approve(this.token, initialSupply);
  69. const tx = await this.token.connect(this.initialHolder).depositFor(this.recipient, initialSupply);
  70. await expect(tx)
  71. .to.emit(this.underlying, 'Transfer')
  72. .withArgs(this.initialHolder, this.token, initialSupply)
  73. .to.emit(this.token, 'Transfer')
  74. .withArgs(ethers.ZeroAddress, this.recipient, initialSupply);
  75. await expect(tx).to.changeTokenBalances(
  76. this.underlying,
  77. [this.initialHolder, this.token],
  78. [-initialSupply, initialSupply],
  79. );
  80. await expect(tx).to.changeTokenBalances(this.token, [this.initialHolder, this.recipient], [0, initialSupply]);
  81. });
  82. it('reverts minting to the wrapper contract', async function () {
  83. await this.underlying.connect(this.initialHolder).approve(this.token, ethers.MaxUint256);
  84. await expect(this.token.connect(this.initialHolder).depositFor(this.token, ethers.MaxUint256))
  85. .to.be.revertedWithCustomError(this.token, 'ERC20InvalidReceiver')
  86. .withArgs(this.token);
  87. });
  88. });
  89. describe('withdraw', function () {
  90. beforeEach(async function () {
  91. await this.underlying.connect(this.initialHolder).approve(this.token, initialSupply);
  92. await this.token.connect(this.initialHolder).depositFor(this.initialHolder, initialSupply);
  93. });
  94. it('reverts when inssuficient balance', async function () {
  95. await expect(this.token.connect(this.initialHolder).withdrawTo(this.initialHolder, ethers.MaxInt256))
  96. .to.be.revertedWithCustomError(this.token, 'ERC20InsufficientBalance')
  97. .withArgs(this.initialHolder, initialSupply, ethers.MaxInt256);
  98. });
  99. it('executes when operation is valid', async function () {
  100. const value = 42n;
  101. const tx = await this.token.connect(this.initialHolder).withdrawTo(this.initialHolder, value);
  102. await expect(tx)
  103. .to.emit(this.underlying, 'Transfer')
  104. .withArgs(this.token, this.initialHolder, value)
  105. .to.emit(this.token, 'Transfer')
  106. .withArgs(this.initialHolder, ethers.ZeroAddress, value);
  107. await expect(tx).to.changeTokenBalances(this.underlying, [this.token, this.initialHolder], [-value, value]);
  108. await expect(tx).to.changeTokenBalance(this.token, this.initialHolder, -value);
  109. });
  110. it('entire balance', async function () {
  111. const tx = await this.token.connect(this.initialHolder).withdrawTo(this.initialHolder, initialSupply);
  112. await expect(tx)
  113. .to.emit(this.underlying, 'Transfer')
  114. .withArgs(this.token, this.initialHolder, initialSupply)
  115. .to.emit(this.token, 'Transfer')
  116. .withArgs(this.initialHolder, ethers.ZeroAddress, initialSupply);
  117. await expect(tx).to.changeTokenBalances(
  118. this.underlying,
  119. [this.token, this.initialHolder],
  120. [-initialSupply, initialSupply],
  121. );
  122. await expect(tx).to.changeTokenBalance(this.token, this.initialHolder, -initialSupply);
  123. });
  124. it('to other account', async function () {
  125. const tx = await this.token.connect(this.initialHolder).withdrawTo(this.recipient, initialSupply);
  126. await expect(tx)
  127. .to.emit(this.underlying, 'Transfer')
  128. .withArgs(this.token, this.recipient, initialSupply)
  129. .to.emit(this.token, 'Transfer')
  130. .withArgs(this.initialHolder, ethers.ZeroAddress, initialSupply);
  131. await expect(tx).to.changeTokenBalances(
  132. this.underlying,
  133. [this.token, this.initialHolder, this.recipient],
  134. [-initialSupply, 0, initialSupply],
  135. );
  136. await expect(tx).to.changeTokenBalance(this.token, this.initialHolder, -initialSupply);
  137. });
  138. it('reverts withdrawing to the wrapper contract', async function () {
  139. await expect(this.token.connect(this.initialHolder).withdrawTo(this.token, initialSupply))
  140. .to.be.revertedWithCustomError(this.token, 'ERC20InvalidReceiver')
  141. .withArgs(this.token);
  142. });
  143. });
  144. describe('recover', function () {
  145. it('nothing to recover', async function () {
  146. await this.underlying.connect(this.initialHolder).approve(this.token, initialSupply);
  147. await this.token.connect(this.initialHolder).depositFor(this.initialHolder, initialSupply);
  148. const tx = await this.token.$_recover(this.recipient);
  149. await expect(tx).to.emit(this.token, 'Transfer').withArgs(ethers.ZeroAddress, this.recipient, 0n);
  150. await expect(tx).to.changeTokenBalance(this.token, this.recipient, 0);
  151. });
  152. it('something to recover', async function () {
  153. await this.underlying.connect(this.initialHolder).transfer(this.token, initialSupply);
  154. const tx = await this.token.$_recover(this.recipient);
  155. await expect(tx).to.emit(this.token, 'Transfer').withArgs(ethers.ZeroAddress, this.recipient, initialSupply);
  156. await expect(tx).to.changeTokenBalance(this.token, this.recipient, initialSupply);
  157. });
  158. });
  159. describe('erc20 behaviour', function () {
  160. beforeEach(async function () {
  161. await this.underlying.connect(this.initialHolder).approve(this.token, initialSupply);
  162. await this.token.connect(this.initialHolder).depositFor(this.initialHolder, initialSupply);
  163. });
  164. shouldBehaveLikeERC20(initialSupply);
  165. });
  166. });