SafeERC20.test.js 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255
  1. const { constants, expectRevert } = require('@openzeppelin/test-helpers');
  2. const ERC20ReturnFalseMock = artifacts.require('ERC20ReturnFalseMock');
  3. const ERC20ReturnTrueMock = artifacts.require('ERC20ReturnTrueMock');
  4. const ERC20NoReturnMock = artifacts.require('ERC20NoReturnMock');
  5. const ERC20PermitNoRevertMock = artifacts.require('ERC20PermitNoRevertMock');
  6. const SafeERC20Wrapper = artifacts.require('SafeERC20Wrapper');
  7. const { EIP712Domain, Permit } = require('../../../helpers/eip712');
  8. const { fromRpcSig } = require('ethereumjs-util');
  9. const ethSigUtil = require('eth-sig-util');
  10. const Wallet = require('ethereumjs-wallet').default;
  11. contract('SafeERC20', function (accounts) {
  12. const [ hasNoCode ] = accounts;
  13. describe('with address that has no contract code', function () {
  14. beforeEach(async function () {
  15. this.wrapper = await SafeERC20Wrapper.new(hasNoCode);
  16. });
  17. shouldRevertOnAllCalls('Address: call to non-contract');
  18. });
  19. describe('with token that returns false on all calls', function () {
  20. beforeEach(async function () {
  21. this.wrapper = await SafeERC20Wrapper.new((await ERC20ReturnFalseMock.new()).address);
  22. });
  23. shouldRevertOnAllCalls('SafeERC20: ERC20 operation did not succeed');
  24. });
  25. describe('with token that returns true on all calls', function () {
  26. beforeEach(async function () {
  27. this.wrapper = await SafeERC20Wrapper.new((await ERC20ReturnTrueMock.new()).address);
  28. });
  29. shouldOnlyRevertOnErrors();
  30. });
  31. describe('with token that returns no boolean values', function () {
  32. beforeEach(async function () {
  33. this.wrapper = await SafeERC20Wrapper.new((await ERC20NoReturnMock.new()).address);
  34. });
  35. shouldOnlyRevertOnErrors();
  36. });
  37. describe('with token that doesn\'t revert on invalid permit', function () {
  38. const wallet = Wallet.generate();
  39. const owner = wallet.getAddressString();
  40. const spender = hasNoCode;
  41. beforeEach(async function () {
  42. this.token = await ERC20PermitNoRevertMock.new();
  43. this.wrapper = await SafeERC20Wrapper.new(this.token.address);
  44. const chainId = await this.token.getChainId();
  45. this.data = {
  46. primaryType: 'Permit',
  47. types: { EIP712Domain, Permit },
  48. domain: { name: 'ERC20PermitNoRevertMock', version: '1', chainId, verifyingContract: this.token.address },
  49. message: { owner, spender, value: '42', nonce: '0', deadline: constants.MAX_UINT256 },
  50. };
  51. this.signature = fromRpcSig(ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data: this.data }));
  52. });
  53. it('accepts owner signature', async function () {
  54. expect(await this.token.nonces(owner)).to.be.bignumber.equal('0');
  55. expect(await this.token.allowance(owner, spender)).to.be.bignumber.equal('0');
  56. await this.wrapper.permit(
  57. this.data.message.owner,
  58. this.data.message.spender,
  59. this.data.message.value,
  60. this.data.message.deadline,
  61. this.signature.v,
  62. this.signature.r,
  63. this.signature.s,
  64. );
  65. expect(await this.token.nonces(owner)).to.be.bignumber.equal('1');
  66. expect(await this.token.allowance(owner, spender)).to.be.bignumber.equal(this.data.message.value);
  67. });
  68. it('revert on reused signature', async function () {
  69. expect(await this.token.nonces(owner)).to.be.bignumber.equal('0');
  70. // use valid signature and consume nounce
  71. await this.wrapper.permit(
  72. this.data.message.owner,
  73. this.data.message.spender,
  74. this.data.message.value,
  75. this.data.message.deadline,
  76. this.signature.v,
  77. this.signature.r,
  78. this.signature.s,
  79. );
  80. expect(await this.token.nonces(owner)).to.be.bignumber.equal('1');
  81. // invalid call does not revert for this token implementation
  82. await this.token.permit(
  83. this.data.message.owner,
  84. this.data.message.spender,
  85. this.data.message.value,
  86. this.data.message.deadline,
  87. this.signature.v,
  88. this.signature.r,
  89. this.signature.s,
  90. );
  91. expect(await this.token.nonces(owner)).to.be.bignumber.equal('1');
  92. // invalid call revert when called through the SafeERC20 library
  93. await expectRevert(
  94. this.wrapper.permit(
  95. this.data.message.owner,
  96. this.data.message.spender,
  97. this.data.message.value,
  98. this.data.message.deadline,
  99. this.signature.v,
  100. this.signature.r,
  101. this.signature.s,
  102. ),
  103. 'SafeERC20: permit did not succeed',
  104. );
  105. expect(await this.token.nonces(owner)).to.be.bignumber.equal('1');
  106. });
  107. it('revert on invalid signature', async function () {
  108. // signature that is not valid for owner
  109. const invalidSignature = {
  110. v: 27,
  111. r: '0x71753dc5ecb5b4bfc0e3bc530d79ce5988760ed3f3a234c86a5546491f540775',
  112. s: '0x0049cedee5aed990aabed5ad6a9f6e3c565b63379894b5fa8b512eb2b79e485d',
  113. };
  114. // invalid call does not revert for this token implementation
  115. await this.token.permit(
  116. this.data.message.owner,
  117. this.data.message.spender,
  118. this.data.message.value,
  119. this.data.message.deadline,
  120. invalidSignature.v,
  121. invalidSignature.r,
  122. invalidSignature.s,
  123. );
  124. // invalid call revert when called through the SafeERC20 library
  125. await expectRevert(
  126. this.wrapper.permit(
  127. this.data.message.owner,
  128. this.data.message.spender,
  129. this.data.message.value,
  130. this.data.message.deadline,
  131. invalidSignature.v,
  132. invalidSignature.r,
  133. invalidSignature.s,
  134. ),
  135. 'SafeERC20: permit did not succeed',
  136. );
  137. });
  138. });
  139. });
  140. function shouldRevertOnAllCalls (reason) {
  141. it('reverts on transfer', async function () {
  142. await expectRevert(this.wrapper.transfer(), reason);
  143. });
  144. it('reverts on transferFrom', async function () {
  145. await expectRevert(this.wrapper.transferFrom(), reason);
  146. });
  147. it('reverts on approve', async function () {
  148. await expectRevert(this.wrapper.approve(0), reason);
  149. });
  150. it('reverts on increaseAllowance', async function () {
  151. // [TODO] make sure it's reverting for the right reason
  152. await expectRevert.unspecified(this.wrapper.increaseAllowance(0));
  153. });
  154. it('reverts on decreaseAllowance', async function () {
  155. // [TODO] make sure it's reverting for the right reason
  156. await expectRevert.unspecified(this.wrapper.decreaseAllowance(0));
  157. });
  158. }
  159. function shouldOnlyRevertOnErrors () {
  160. it('doesn\'t revert on transfer', async function () {
  161. await this.wrapper.transfer();
  162. });
  163. it('doesn\'t revert on transferFrom', async function () {
  164. await this.wrapper.transferFrom();
  165. });
  166. describe('approvals', function () {
  167. context('with zero allowance', function () {
  168. beforeEach(async function () {
  169. await this.wrapper.setAllowance(0);
  170. });
  171. it('doesn\'t revert when approving a non-zero allowance', async function () {
  172. await this.wrapper.approve(100);
  173. });
  174. it('doesn\'t revert when approving a zero allowance', async function () {
  175. await this.wrapper.approve(0);
  176. });
  177. it('doesn\'t revert when increasing the allowance', async function () {
  178. await this.wrapper.increaseAllowance(10);
  179. });
  180. it('reverts when decreasing the allowance', async function () {
  181. await expectRevert(
  182. this.wrapper.decreaseAllowance(10),
  183. 'SafeERC20: decreased allowance below zero',
  184. );
  185. });
  186. });
  187. context('with non-zero allowance', function () {
  188. beforeEach(async function () {
  189. await this.wrapper.setAllowance(100);
  190. });
  191. it('reverts when approving a non-zero allowance', async function () {
  192. await expectRevert(
  193. this.wrapper.approve(20),
  194. 'SafeERC20: approve from non-zero to non-zero allowance',
  195. );
  196. });
  197. it('doesn\'t revert when approving a zero allowance', async function () {
  198. await this.wrapper.approve(0);
  199. });
  200. it('doesn\'t revert when increasing the allowance', async function () {
  201. await this.wrapper.increaseAllowance(10);
  202. });
  203. it('doesn\'t revert when decreasing the allowance to a positive value', async function () {
  204. await this.wrapper.decreaseAllowance(50);
  205. });
  206. it('reverts when decreasing the allowance to a negative value', async function () {
  207. await expectRevert(
  208. this.wrapper.decreaseAllowance(200),
  209. 'SafeERC20: decreased allowance below zero',
  210. );
  211. });
  212. });
  213. });
  214. }