ERC20Wrapper.test.js 8.3 KB

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