SafeERC20.test.js 9.3 KB

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