ERC20.behavior.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340
  1. const { BN, constants, expectEvent } = require('@openzeppelin/test-helpers');
  2. const { expect } = require('chai');
  3. const { ZERO_ADDRESS, MAX_UINT256 } = constants;
  4. const { expectRevertCustomError } = require('../../helpers/customError');
  5. function shouldBehaveLikeERC20(initialSupply, accounts, opts = {}) {
  6. const [initialHolder, recipient, anotherAccount] = accounts;
  7. const { forcedApproval } = opts;
  8. describe('total supply', function () {
  9. it('returns the total token value', async function () {
  10. expect(await this.token.totalSupply()).to.be.bignumber.equal(initialSupply);
  11. });
  12. });
  13. describe('balanceOf', function () {
  14. describe('when the requested account has no tokens', function () {
  15. it('returns zero', async function () {
  16. expect(await this.token.balanceOf(anotherAccount)).to.be.bignumber.equal('0');
  17. });
  18. });
  19. describe('when the requested account has some tokens', function () {
  20. it('returns the total token value', async function () {
  21. expect(await this.token.balanceOf(initialHolder)).to.be.bignumber.equal(initialSupply);
  22. });
  23. });
  24. });
  25. describe('transfer', function () {
  26. shouldBehaveLikeERC20Transfer(initialHolder, recipient, initialSupply, function (from, to, value) {
  27. return this.token.transfer(to, value, { from });
  28. });
  29. });
  30. describe('transfer from', function () {
  31. const spender = recipient;
  32. describe('when the token owner is not the zero address', function () {
  33. const tokenOwner = initialHolder;
  34. describe('when the recipient is not the zero address', function () {
  35. const to = anotherAccount;
  36. describe('when the spender has enough allowance', function () {
  37. beforeEach(async function () {
  38. await this.token.approve(spender, initialSupply, { from: initialHolder });
  39. });
  40. describe('when the token owner has enough balance', function () {
  41. const value = initialSupply;
  42. it('transfers the requested value', async function () {
  43. await this.token.transferFrom(tokenOwner, to, value, { from: spender });
  44. expect(await this.token.balanceOf(tokenOwner)).to.be.bignumber.equal('0');
  45. expect(await this.token.balanceOf(to)).to.be.bignumber.equal(value);
  46. });
  47. it('decreases the spender allowance', async function () {
  48. await this.token.transferFrom(tokenOwner, to, value, { from: spender });
  49. expect(await this.token.allowance(tokenOwner, spender)).to.be.bignumber.equal('0');
  50. });
  51. it('emits a transfer event', async function () {
  52. expectEvent(await this.token.transferFrom(tokenOwner, to, value, { from: spender }), 'Transfer', {
  53. from: tokenOwner,
  54. to: to,
  55. value: value,
  56. });
  57. });
  58. if (forcedApproval) {
  59. it('emits an approval event', async function () {
  60. expectEvent(await this.token.transferFrom(tokenOwner, to, value, { from: spender }), 'Approval', {
  61. owner: tokenOwner,
  62. spender: spender,
  63. value: await this.token.allowance(tokenOwner, spender),
  64. });
  65. });
  66. } else {
  67. it('does not emit an approval event', async function () {
  68. expectEvent.notEmitted(
  69. await this.token.transferFrom(tokenOwner, to, value, { from: spender }),
  70. 'Approval',
  71. );
  72. });
  73. }
  74. });
  75. describe('when the token owner does not have enough balance', function () {
  76. const value = initialSupply;
  77. beforeEach('reducing balance', async function () {
  78. await this.token.transfer(to, 1, { from: tokenOwner });
  79. });
  80. it('reverts', async function () {
  81. await expectRevertCustomError(
  82. this.token.transferFrom(tokenOwner, to, value, { from: spender }),
  83. 'ERC20InsufficientBalance',
  84. [tokenOwner, value - 1, value],
  85. );
  86. });
  87. });
  88. });
  89. describe('when the spender does not have enough allowance', function () {
  90. const allowance = initialSupply.subn(1);
  91. beforeEach(async function () {
  92. await this.token.approve(spender, allowance, { from: tokenOwner });
  93. });
  94. describe('when the token owner has enough balance', function () {
  95. const value = initialSupply;
  96. it('reverts', async function () {
  97. await expectRevertCustomError(
  98. this.token.transferFrom(tokenOwner, to, value, { from: spender }),
  99. 'ERC20InsufficientAllowance',
  100. [spender, allowance, value],
  101. );
  102. });
  103. });
  104. describe('when the token owner does not have enough balance', function () {
  105. const value = allowance;
  106. beforeEach('reducing balance', async function () {
  107. await this.token.transfer(to, 2, { from: tokenOwner });
  108. });
  109. it('reverts', async function () {
  110. await expectRevertCustomError(
  111. this.token.transferFrom(tokenOwner, to, value, { from: spender }),
  112. 'ERC20InsufficientBalance',
  113. [tokenOwner, value - 1, value],
  114. );
  115. });
  116. });
  117. });
  118. describe('when the spender has unlimited allowance', function () {
  119. beforeEach(async function () {
  120. await this.token.approve(spender, MAX_UINT256, { from: initialHolder });
  121. });
  122. it('does not decrease the spender allowance', async function () {
  123. await this.token.transferFrom(tokenOwner, to, 1, { from: spender });
  124. expect(await this.token.allowance(tokenOwner, spender)).to.be.bignumber.equal(MAX_UINT256);
  125. });
  126. it('does not emit an approval event', async function () {
  127. expectEvent.notEmitted(await this.token.transferFrom(tokenOwner, to, 1, { from: spender }), 'Approval');
  128. });
  129. });
  130. });
  131. describe('when the recipient is the zero address', function () {
  132. const value = initialSupply;
  133. const to = ZERO_ADDRESS;
  134. beforeEach(async function () {
  135. await this.token.approve(spender, value, { from: tokenOwner });
  136. });
  137. it('reverts', async function () {
  138. await expectRevertCustomError(
  139. this.token.transferFrom(tokenOwner, to, value, { from: spender }),
  140. 'ERC20InvalidReceiver',
  141. [ZERO_ADDRESS],
  142. );
  143. });
  144. });
  145. });
  146. describe('when the token owner is the zero address', function () {
  147. const value = 0;
  148. const tokenOwner = ZERO_ADDRESS;
  149. const to = recipient;
  150. it('reverts', async function () {
  151. await expectRevertCustomError(
  152. this.token.transferFrom(tokenOwner, to, value, { from: spender }),
  153. 'ERC20InvalidApprover',
  154. [ZERO_ADDRESS],
  155. );
  156. });
  157. });
  158. });
  159. describe('approve', function () {
  160. shouldBehaveLikeERC20Approve(initialHolder, recipient, initialSupply, function (owner, spender, value) {
  161. return this.token.approve(spender, value, { from: owner });
  162. });
  163. });
  164. }
  165. function shouldBehaveLikeERC20Transfer(from, to, balance, transfer) {
  166. describe('when the recipient is not the zero address', function () {
  167. describe('when the sender does not have enough balance', function () {
  168. const value = balance.addn(1);
  169. it('reverts', async function () {
  170. await expectRevertCustomError(transfer.call(this, from, to, value), 'ERC20InsufficientBalance', [
  171. from,
  172. balance,
  173. value,
  174. ]);
  175. });
  176. });
  177. describe('when the sender transfers all balance', function () {
  178. const value = balance;
  179. it('transfers the requested value', async function () {
  180. await transfer.call(this, from, to, value);
  181. expect(await this.token.balanceOf(from)).to.be.bignumber.equal('0');
  182. expect(await this.token.balanceOf(to)).to.be.bignumber.equal(value);
  183. });
  184. it('emits a transfer event', async function () {
  185. expectEvent(await transfer.call(this, from, to, value), 'Transfer', { from, to, value: value });
  186. });
  187. });
  188. describe('when the sender transfers zero tokens', function () {
  189. const value = new BN('0');
  190. it('transfers the requested value', async function () {
  191. await transfer.call(this, from, to, value);
  192. expect(await this.token.balanceOf(from)).to.be.bignumber.equal(balance);
  193. expect(await this.token.balanceOf(to)).to.be.bignumber.equal('0');
  194. });
  195. it('emits a transfer event', async function () {
  196. expectEvent(await transfer.call(this, from, to, value), 'Transfer', { from, to, value: value });
  197. });
  198. });
  199. });
  200. describe('when the recipient is the zero address', function () {
  201. it('reverts', async function () {
  202. await expectRevertCustomError(transfer.call(this, from, ZERO_ADDRESS, balance), 'ERC20InvalidReceiver', [
  203. ZERO_ADDRESS,
  204. ]);
  205. });
  206. });
  207. }
  208. function shouldBehaveLikeERC20Approve(owner, spender, supply, approve) {
  209. describe('when the spender is not the zero address', function () {
  210. describe('when the sender has enough balance', function () {
  211. const value = supply;
  212. it('emits an approval event', async function () {
  213. expectEvent(await approve.call(this, owner, spender, value), 'Approval', {
  214. owner: owner,
  215. spender: spender,
  216. value: value,
  217. });
  218. });
  219. describe('when there was no approved value before', function () {
  220. it('approves the requested value', async function () {
  221. await approve.call(this, owner, spender, value);
  222. expect(await this.token.allowance(owner, spender)).to.be.bignumber.equal(value);
  223. });
  224. });
  225. describe('when the spender had an approved value', function () {
  226. beforeEach(async function () {
  227. await approve.call(this, owner, spender, new BN(1));
  228. });
  229. it('approves the requested value and replaces the previous one', async function () {
  230. await approve.call(this, owner, spender, value);
  231. expect(await this.token.allowance(owner, spender)).to.be.bignumber.equal(value);
  232. });
  233. });
  234. });
  235. describe('when the sender does not have enough balance', function () {
  236. const value = supply.addn(1);
  237. it('emits an approval event', async function () {
  238. expectEvent(await approve.call(this, owner, spender, value), 'Approval', {
  239. owner: owner,
  240. spender: spender,
  241. value: value,
  242. });
  243. });
  244. describe('when there was no approved value before', function () {
  245. it('approves the requested value', async function () {
  246. await approve.call(this, owner, spender, value);
  247. expect(await this.token.allowance(owner, spender)).to.be.bignumber.equal(value);
  248. });
  249. });
  250. describe('when the spender had an approved value', function () {
  251. beforeEach(async function () {
  252. await approve.call(this, owner, spender, new BN(1));
  253. });
  254. it('approves the requested value and replaces the previous one', async function () {
  255. await approve.call(this, owner, spender, value);
  256. expect(await this.token.allowance(owner, spender)).to.be.bignumber.equal(value);
  257. });
  258. });
  259. });
  260. });
  261. describe('when the spender is the zero address', function () {
  262. it('reverts', async function () {
  263. await expectRevertCustomError(approve.call(this, owner, ZERO_ADDRESS, supply), `ERC20InvalidSpender`, [
  264. ZERO_ADDRESS,
  265. ]);
  266. });
  267. });
  268. }
  269. module.exports = {
  270. shouldBehaveLikeERC20,
  271. shouldBehaveLikeERC20Transfer,
  272. shouldBehaveLikeERC20Approve,
  273. };