draft-ERC20TemporaryApproval.test.js 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142
  1. const { ethers } = require('hardhat');
  2. const { expect } = require('chai');
  3. const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
  4. const { max, min } = require('../../../helpers/math.js');
  5. const { shouldBehaveLikeERC20 } = require('../ERC20.behavior.js');
  6. const name = 'My Token';
  7. const symbol = 'MTKN';
  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 token = await ethers.deployContract('$ERC20TemporaryApproval', [name, symbol]);
  14. await token.$_mint(holder, initialSupply);
  15. const spender = await ethers.deployContract('$Address');
  16. const batch = await ethers.deployContract('BatchCaller');
  17. const getter = await ethers.deployContract('ERC20GetterHelper');
  18. return { accounts, holder, recipient, other, token, spender, batch, getter };
  19. }
  20. describe('ERC20TemporaryApproval', function () {
  21. beforeEach(async function () {
  22. Object.assign(this, await loadFixture(fixture));
  23. });
  24. shouldBehaveLikeERC20(initialSupply);
  25. describe('setting and spending temporary allowance', function () {
  26. beforeEach(async function () {
  27. await this.token.connect(this.holder).transfer(this.batch, initialSupply);
  28. });
  29. for (let {
  30. description,
  31. persistentAllowance,
  32. temporaryAllowance,
  33. amount,
  34. temporaryExpected,
  35. persistentExpected,
  36. } of [
  37. { description: 'can set temporary allowance', temporaryAllowance: 42n },
  38. {
  39. description: 'can set temporary allowance on top of persistent allowance',
  40. temporaryAllowance: 42n,
  41. persistentAllowance: 17n,
  42. },
  43. { description: 'support allowance overflow', temporaryAllowance: ethers.MaxUint256, persistentAllowance: 17n },
  44. { description: 'consuming temporary allowance alone', temporaryAllowance: 42n, amount: 2n },
  45. {
  46. description: 'fallback to persistent allowance if temporary allowance is not sufficient',
  47. temporaryAllowance: 42n,
  48. persistentAllowance: 17n,
  49. amount: 50n,
  50. },
  51. {
  52. description: 'do not reduce infinite temporary allowance #1',
  53. temporaryAllowance: ethers.MaxUint256,
  54. amount: 50n,
  55. temporaryExpected: ethers.MaxUint256,
  56. },
  57. {
  58. description: 'do not reduce infinite temporary allowance #2',
  59. temporaryAllowance: 17n,
  60. persistentAllowance: ethers.MaxUint256,
  61. amount: 50n,
  62. temporaryExpected: ethers.MaxUint256,
  63. persistentExpected: ethers.MaxUint256,
  64. },
  65. ]) {
  66. persistentAllowance ??= 0n;
  67. temporaryAllowance ??= 0n;
  68. amount ??= 0n;
  69. temporaryExpected ??= min(persistentAllowance + temporaryAllowance - amount, ethers.MaxUint256);
  70. persistentExpected ??= persistentAllowance - max(amount - temporaryAllowance, 0n);
  71. it(description, async function () {
  72. await expect(
  73. this.batch.execute(
  74. [
  75. persistentAllowance && {
  76. target: this.token,
  77. value: 0n,
  78. data: this.token.interface.encodeFunctionData('approve', [this.spender.target, persistentAllowance]),
  79. },
  80. temporaryAllowance && {
  81. target: this.token,
  82. value: 0n,
  83. data: this.token.interface.encodeFunctionData('temporaryApprove', [
  84. this.spender.target,
  85. temporaryAllowance,
  86. ]),
  87. },
  88. amount && {
  89. target: this.spender,
  90. value: 0n,
  91. data: this.spender.interface.encodeFunctionData('$functionCall', [
  92. this.token.target,
  93. this.token.interface.encodeFunctionData('transferFrom', [
  94. this.batch.target,
  95. this.recipient.address,
  96. amount,
  97. ]),
  98. ]),
  99. },
  100. {
  101. target: this.getter,
  102. value: 0n,
  103. data: this.getter.interface.encodeFunctionData('allowance', [
  104. this.token.target,
  105. this.batch.target,
  106. this.spender.target,
  107. ]),
  108. },
  109. ].filter(Boolean),
  110. ),
  111. )
  112. .to.emit(this.getter, 'ERC20Allowance')
  113. .withArgs(this.token, this.batch, this.spender, temporaryExpected);
  114. expect(await this.token.allowance(this.batch, this.spender)).to.equal(persistentExpected);
  115. });
  116. }
  117. it('reverts when the recipient is the zero address', async function () {
  118. await expect(this.token.connect(this.holder).temporaryApprove(ethers.ZeroAddress, 1n))
  119. .to.be.revertedWithCustomError(this.token, 'ERC20InvalidSpender')
  120. .withArgs(ethers.ZeroAddress);
  121. });
  122. it('reverts when the token owner is the zero address', async function () {
  123. await expect(this.token.$_temporaryApprove(ethers.ZeroAddress, this.recipient, 1n))
  124. .to.be.revertedWithCustomError(this.token, 'ERC20InvalidApprover')
  125. .withArgs(ethers.ZeroAddress);
  126. });
  127. });
  128. });