ERC20.test.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365
  1. const { BN, constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers');
  2. const { expect } = require('chai');
  3. const { ZERO_ADDRESS } = constants;
  4. const {
  5. shouldBehaveLikeERC20,
  6. shouldBehaveLikeERC20Transfer,
  7. shouldBehaveLikeERC20Approve,
  8. } = require('./ERC20.behavior');
  9. const { expectRevertCustomError } = require('../../helpers/customError');
  10. const ERC20 = artifacts.require('$ERC20');
  11. const ERC20Decimals = artifacts.require('$ERC20DecimalsMock');
  12. contract('ERC20', function (accounts) {
  13. const [initialHolder, recipient, anotherAccount] = accounts;
  14. const name = 'My Token';
  15. const symbol = 'MTKN';
  16. const initialSupply = new BN(100);
  17. beforeEach(async function () {
  18. this.token = await ERC20.new(name, symbol);
  19. await this.token.$_mint(initialHolder, initialSupply);
  20. });
  21. it('has a name', async function () {
  22. expect(await this.token.name()).to.equal(name);
  23. });
  24. it('has a symbol', async function () {
  25. expect(await this.token.symbol()).to.equal(symbol);
  26. });
  27. it('has 18 decimals', async function () {
  28. expect(await this.token.decimals()).to.be.bignumber.equal('18');
  29. });
  30. describe('set decimals', function () {
  31. const decimals = new BN(6);
  32. it('can set decimals during construction', async function () {
  33. const token = await ERC20Decimals.new(name, symbol, decimals);
  34. expect(await token.decimals()).to.be.bignumber.equal(decimals);
  35. });
  36. });
  37. shouldBehaveLikeERC20('ERC20', initialSupply, initialHolder, recipient, anotherAccount);
  38. describe('decrease allowance', function () {
  39. describe('when the spender is not the zero address', function () {
  40. const spender = recipient;
  41. function shouldDecreaseApproval(amount) {
  42. describe('when there was no approved amount before', function () {
  43. it('reverts', async function () {
  44. const allowance = await this.token.allowance(initialHolder, spender);
  45. await expectRevertCustomError(
  46. this.token.decreaseAllowance(spender, amount, { from: initialHolder }),
  47. 'ERC20FailedDecreaseAllowance',
  48. [spender, allowance, amount],
  49. );
  50. });
  51. });
  52. describe('when the spender had an approved amount', function () {
  53. const approvedAmount = amount;
  54. beforeEach(async function () {
  55. await this.token.approve(spender, approvedAmount, { from: initialHolder });
  56. });
  57. it('emits an approval event', async function () {
  58. expectEvent(
  59. await this.token.decreaseAllowance(spender, approvedAmount, { from: initialHolder }),
  60. 'Approval',
  61. { owner: initialHolder, spender: spender, value: new BN(0) },
  62. );
  63. });
  64. it('decreases the spender allowance subtracting the requested amount', async function () {
  65. await this.token.decreaseAllowance(spender, approvedAmount.subn(1), { from: initialHolder });
  66. expect(await this.token.allowance(initialHolder, spender)).to.be.bignumber.equal('1');
  67. });
  68. it('sets the allowance to zero when all allowance is removed', async function () {
  69. await this.token.decreaseAllowance(spender, approvedAmount, { from: initialHolder });
  70. expect(await this.token.allowance(initialHolder, spender)).to.be.bignumber.equal('0');
  71. });
  72. it('reverts when more than the full allowance is removed', async function () {
  73. await expectRevertCustomError(
  74. this.token.decreaseAllowance(spender, approvedAmount.addn(1), { from: initialHolder }),
  75. 'ERC20FailedDecreaseAllowance',
  76. [spender, approvedAmount, approvedAmount.addn(1)],
  77. );
  78. });
  79. });
  80. }
  81. describe('when the sender has enough balance', function () {
  82. const amount = initialSupply;
  83. shouldDecreaseApproval(amount);
  84. });
  85. describe('when the sender does not have enough balance', function () {
  86. const amount = initialSupply.addn(1);
  87. shouldDecreaseApproval(amount);
  88. });
  89. });
  90. describe('when the spender is the zero address', function () {
  91. const amount = initialSupply;
  92. const spender = ZERO_ADDRESS;
  93. it('reverts', async function () {
  94. await expectRevertCustomError(
  95. this.token.decreaseAllowance(spender, amount, { from: initialHolder }),
  96. 'ERC20FailedDecreaseAllowance',
  97. [spender, 0, amount],
  98. );
  99. });
  100. });
  101. });
  102. describe('increase allowance', function () {
  103. const amount = initialSupply;
  104. describe('when the spender is not the zero address', function () {
  105. const spender = recipient;
  106. describe('when the sender has enough balance', function () {
  107. it('emits an approval event', async function () {
  108. expectEvent(await this.token.increaseAllowance(spender, amount, { from: initialHolder }), 'Approval', {
  109. owner: initialHolder,
  110. spender: spender,
  111. value: amount,
  112. });
  113. });
  114. describe('when there was no approved amount before', function () {
  115. it('approves the requested amount', async function () {
  116. await this.token.increaseAllowance(spender, amount, { from: initialHolder });
  117. expect(await this.token.allowance(initialHolder, spender)).to.be.bignumber.equal(amount);
  118. });
  119. });
  120. describe('when the spender had an approved amount', function () {
  121. beforeEach(async function () {
  122. await this.token.approve(spender, new BN(1), { from: initialHolder });
  123. });
  124. it('increases the spender allowance adding the requested amount', async function () {
  125. await this.token.increaseAllowance(spender, amount, { from: initialHolder });
  126. expect(await this.token.allowance(initialHolder, spender)).to.be.bignumber.equal(amount.addn(1));
  127. });
  128. });
  129. });
  130. describe('when the sender does not have enough balance', function () {
  131. const amount = initialSupply.addn(1);
  132. it('emits an approval event', async function () {
  133. expectEvent(await this.token.increaseAllowance(spender, amount, { from: initialHolder }), 'Approval', {
  134. owner: initialHolder,
  135. spender: spender,
  136. value: amount,
  137. });
  138. });
  139. describe('when there was no approved amount before', function () {
  140. it('approves the requested amount', async function () {
  141. await this.token.increaseAllowance(spender, amount, { from: initialHolder });
  142. expect(await this.token.allowance(initialHolder, spender)).to.be.bignumber.equal(amount);
  143. });
  144. });
  145. describe('when the spender had an approved amount', function () {
  146. beforeEach(async function () {
  147. await this.token.approve(spender, new BN(1), { from: initialHolder });
  148. });
  149. it('increases the spender allowance adding the requested amount', async function () {
  150. await this.token.increaseAllowance(spender, amount, { from: initialHolder });
  151. expect(await this.token.allowance(initialHolder, spender)).to.be.bignumber.equal(amount.addn(1));
  152. });
  153. });
  154. });
  155. });
  156. describe('when the spender is the zero address', function () {
  157. const spender = ZERO_ADDRESS;
  158. it('reverts', async function () {
  159. await expectRevertCustomError(
  160. this.token.increaseAllowance(spender, amount, { from: initialHolder }),
  161. 'ERC20InvalidSpender',
  162. [ZERO_ADDRESS],
  163. );
  164. });
  165. });
  166. });
  167. describe('_mint', function () {
  168. const amount = new BN(50);
  169. it('rejects a null account', async function () {
  170. await expectRevertCustomError(this.token.$_mint(ZERO_ADDRESS, amount), 'ERC20InvalidReceiver', [ZERO_ADDRESS]);
  171. });
  172. it('rejects overflow', async function () {
  173. const maxUint256 = new BN('2').pow(new BN(256)).subn(1);
  174. await expectRevert(
  175. this.token.$_mint(recipient, maxUint256),
  176. 'reverted with panic code 0x11 (Arithmetic operation underflowed or overflowed outside of an unchecked block)',
  177. );
  178. });
  179. describe('for a non zero account', function () {
  180. beforeEach('minting', async function () {
  181. this.receipt = await this.token.$_mint(recipient, amount);
  182. });
  183. it('increments totalSupply', async function () {
  184. const expectedSupply = initialSupply.add(amount);
  185. expect(await this.token.totalSupply()).to.be.bignumber.equal(expectedSupply);
  186. });
  187. it('increments recipient balance', async function () {
  188. expect(await this.token.balanceOf(recipient)).to.be.bignumber.equal(amount);
  189. });
  190. it('emits Transfer event', async function () {
  191. const event = expectEvent(this.receipt, 'Transfer', { from: ZERO_ADDRESS, to: recipient });
  192. expect(event.args.value).to.be.bignumber.equal(amount);
  193. });
  194. });
  195. });
  196. describe('_burn', function () {
  197. it('rejects a null account', async function () {
  198. await expectRevertCustomError(this.token.$_burn(ZERO_ADDRESS, new BN(1)), 'ERC20InvalidSender', [ZERO_ADDRESS]);
  199. });
  200. describe('for a non zero account', function () {
  201. it('rejects burning more than balance', async function () {
  202. await expectRevertCustomError(
  203. this.token.$_burn(initialHolder, initialSupply.addn(1)),
  204. 'ERC20InsufficientBalance',
  205. [initialHolder, initialSupply, initialSupply.addn(1)],
  206. );
  207. });
  208. const describeBurn = function (description, amount) {
  209. describe(description, function () {
  210. beforeEach('burning', async function () {
  211. this.receipt = await this.token.$_burn(initialHolder, amount);
  212. });
  213. it('decrements totalSupply', async function () {
  214. const expectedSupply = initialSupply.sub(amount);
  215. expect(await this.token.totalSupply()).to.be.bignumber.equal(expectedSupply);
  216. });
  217. it('decrements initialHolder balance', async function () {
  218. const expectedBalance = initialSupply.sub(amount);
  219. expect(await this.token.balanceOf(initialHolder)).to.be.bignumber.equal(expectedBalance);
  220. });
  221. it('emits Transfer event', async function () {
  222. const event = expectEvent(this.receipt, 'Transfer', { from: initialHolder, to: ZERO_ADDRESS });
  223. expect(event.args.value).to.be.bignumber.equal(amount);
  224. });
  225. });
  226. };
  227. describeBurn('for entire balance', initialSupply);
  228. describeBurn('for less amount than balance', initialSupply.subn(1));
  229. });
  230. });
  231. describe('_update', function () {
  232. const amount = new BN(1);
  233. it('from is the zero address', async function () {
  234. const balanceBefore = await this.token.balanceOf(initialHolder);
  235. const totalSupply = await this.token.totalSupply();
  236. expectEvent(await this.token.$_update(ZERO_ADDRESS, initialHolder, amount), 'Transfer', {
  237. from: ZERO_ADDRESS,
  238. to: initialHolder,
  239. value: amount,
  240. });
  241. expect(await this.token.totalSupply()).to.be.bignumber.equal(totalSupply.add(amount));
  242. expect(await this.token.balanceOf(initialHolder)).to.be.bignumber.equal(balanceBefore.add(amount));
  243. });
  244. it('to is the zero address', async function () {
  245. const balanceBefore = await this.token.balanceOf(initialHolder);
  246. const totalSupply = await this.token.totalSupply();
  247. expectEvent(await this.token.$_update(initialHolder, ZERO_ADDRESS, amount), 'Transfer', {
  248. from: initialHolder,
  249. to: ZERO_ADDRESS,
  250. value: amount,
  251. });
  252. expect(await this.token.totalSupply()).to.be.bignumber.equal(totalSupply.sub(amount));
  253. expect(await this.token.balanceOf(initialHolder)).to.be.bignumber.equal(balanceBefore.sub(amount));
  254. });
  255. it('from and to are the zero address', async function () {
  256. const totalSupply = await this.token.totalSupply();
  257. await this.token.$_update(ZERO_ADDRESS, ZERO_ADDRESS, amount);
  258. expect(await this.token.totalSupply()).to.be.bignumber.equal(totalSupply);
  259. expectEvent(await this.token.$_update(ZERO_ADDRESS, ZERO_ADDRESS, amount), 'Transfer', {
  260. from: ZERO_ADDRESS,
  261. to: ZERO_ADDRESS,
  262. value: amount,
  263. });
  264. });
  265. });
  266. describe('_transfer', function () {
  267. shouldBehaveLikeERC20Transfer('ERC20', initialHolder, recipient, initialSupply, function (from, to, amount) {
  268. return this.token.$_transfer(from, to, amount);
  269. });
  270. describe('when the sender is the zero address', function () {
  271. it('reverts', async function () {
  272. await expectRevertCustomError(
  273. this.token.$_transfer(ZERO_ADDRESS, recipient, initialSupply),
  274. 'ERC20InvalidSender',
  275. [ZERO_ADDRESS],
  276. );
  277. });
  278. });
  279. });
  280. describe('_approve', function () {
  281. shouldBehaveLikeERC20Approve('ERC20', initialHolder, recipient, initialSupply, function (owner, spender, amount) {
  282. return this.token.$_approve(owner, spender, amount);
  283. });
  284. describe('when the owner is the zero address', function () {
  285. it('reverts', async function () {
  286. await expectRevertCustomError(
  287. this.token.$_approve(ZERO_ADDRESS, recipient, initialSupply),
  288. 'ERC20InvalidApprover',
  289. [ZERO_ADDRESS],
  290. );
  291. });
  292. });
  293. });
  294. });