SafeERC20.test.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357
  1. const { constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers');
  2. const SafeERC20 = artifacts.require('$SafeERC20');
  3. const ERC20ReturnFalseMock = artifacts.require('$ERC20ReturnFalseMock');
  4. const ERC20ReturnTrueMock = artifacts.require('$ERC20'); // default implementation returns true
  5. const ERC20NoReturnMock = artifacts.require('$ERC20NoReturnMock');
  6. const ERC20PermitNoRevertMock = artifacts.require('$ERC20PermitNoRevertMock');
  7. const ERC20ForceApproveMock = artifacts.require('$ERC20ForceApproveMock');
  8. const { getDomain, domainType, Permit } = require('../../../helpers/eip712');
  9. const { expectRevertCustomError } = require('../../../helpers/customError');
  10. const { fromRpcSig } = require('ethereumjs-util');
  11. const ethSigUtil = require('eth-sig-util');
  12. const Wallet = require('ethereumjs-wallet').default;
  13. const name = 'ERC20Mock';
  14. const symbol = 'ERC20Mock';
  15. contract('SafeERC20', function (accounts) {
  16. const [hasNoCode, receiver, spender] = accounts;
  17. before(async function () {
  18. this.mock = await SafeERC20.new();
  19. });
  20. describe('with address that has no contract code', function () {
  21. beforeEach(async function () {
  22. this.token = { address: hasNoCode };
  23. });
  24. it('reverts on transfer', async function () {
  25. await expectRevertCustomError(this.mock.$safeTransfer(this.token.address, receiver, 0), 'AddressEmptyCode', [
  26. this.token.address,
  27. ]);
  28. });
  29. it('reverts on transferFrom', async function () {
  30. await expectRevertCustomError(
  31. this.mock.$safeTransferFrom(this.token.address, this.mock.address, receiver, 0),
  32. 'AddressEmptyCode',
  33. [this.token.address],
  34. );
  35. });
  36. it('reverts on increaseAllowance', async function () {
  37. // [TODO] make sure it's reverting for the right reason
  38. await expectRevert.unspecified(this.mock.$safeIncreaseAllowance(this.token.address, spender, 0));
  39. });
  40. it('reverts on decreaseAllowance', async function () {
  41. // [TODO] make sure it's reverting for the right reason
  42. await expectRevert.unspecified(this.mock.$safeDecreaseAllowance(this.token.address, spender, 0));
  43. });
  44. it('reverts on forceApprove', async function () {
  45. await expectRevertCustomError(this.mock.$forceApprove(this.token.address, spender, 0), 'AddressEmptyCode', [
  46. this.token.address,
  47. ]);
  48. });
  49. });
  50. describe('with token that returns false on all calls', function () {
  51. beforeEach(async function () {
  52. this.token = await ERC20ReturnFalseMock.new(name, symbol);
  53. });
  54. it('reverts on transfer', async function () {
  55. await expectRevertCustomError(
  56. this.mock.$safeTransfer(this.token.address, receiver, 0),
  57. 'SafeERC20FailedOperation',
  58. [this.token.address],
  59. );
  60. });
  61. it('reverts on transferFrom', async function () {
  62. await expectRevertCustomError(
  63. this.mock.$safeTransferFrom(this.token.address, this.mock.address, receiver, 0),
  64. 'SafeERC20FailedOperation',
  65. [this.token.address],
  66. );
  67. });
  68. it('reverts on increaseAllowance', async function () {
  69. // [TODO] make sure it's reverting for the right reason
  70. await expectRevert.unspecified(this.mock.$safeIncreaseAllowance(this.token.address, spender, 0));
  71. });
  72. it('reverts on decreaseAllowance', async function () {
  73. // [TODO] make sure it's reverting for the right reason
  74. await expectRevert.unspecified(this.mock.$safeDecreaseAllowance(this.token.address, spender, 0));
  75. });
  76. it('reverts on forceApprove', async function () {
  77. await expectRevertCustomError(
  78. this.mock.$forceApprove(this.token.address, spender, 0),
  79. 'SafeERC20FailedOperation',
  80. [this.token.address],
  81. );
  82. });
  83. });
  84. describe('with token that returns true on all calls', function () {
  85. beforeEach(async function () {
  86. this.token = await ERC20ReturnTrueMock.new(name, symbol);
  87. });
  88. shouldOnlyRevertOnErrors(accounts);
  89. });
  90. describe('with token that returns no boolean values', function () {
  91. beforeEach(async function () {
  92. this.token = await ERC20NoReturnMock.new(name, symbol);
  93. });
  94. shouldOnlyRevertOnErrors(accounts);
  95. });
  96. describe("with token that doesn't revert on invalid permit", function () {
  97. const wallet = Wallet.generate();
  98. const owner = wallet.getAddressString();
  99. const spender = hasNoCode;
  100. beforeEach(async function () {
  101. this.token = await ERC20PermitNoRevertMock.new(name, symbol, name);
  102. this.data = await getDomain(this.token).then(domain => ({
  103. primaryType: 'Permit',
  104. types: { EIP712Domain: domainType(domain), Permit },
  105. domain,
  106. message: { owner, spender, value: '42', nonce: '0', deadline: constants.MAX_UINT256 },
  107. }));
  108. this.signature = fromRpcSig(ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data: this.data }));
  109. });
  110. it('accepts owner signature', async function () {
  111. expect(await this.token.nonces(owner)).to.be.bignumber.equal('0');
  112. expect(await this.token.allowance(owner, spender)).to.be.bignumber.equal('0');
  113. await this.mock.$safePermit(
  114. this.token.address,
  115. this.data.message.owner,
  116. this.data.message.spender,
  117. this.data.message.value,
  118. this.data.message.deadline,
  119. this.signature.v,
  120. this.signature.r,
  121. this.signature.s,
  122. );
  123. expect(await this.token.nonces(owner)).to.be.bignumber.equal('1');
  124. expect(await this.token.allowance(owner, spender)).to.be.bignumber.equal(this.data.message.value);
  125. });
  126. it('revert on reused signature', async function () {
  127. expect(await this.token.nonces(owner)).to.be.bignumber.equal('0');
  128. // use valid signature and consume nounce
  129. await this.mock.$safePermit(
  130. this.token.address,
  131. this.data.message.owner,
  132. this.data.message.spender,
  133. this.data.message.value,
  134. this.data.message.deadline,
  135. this.signature.v,
  136. this.signature.r,
  137. this.signature.s,
  138. );
  139. expect(await this.token.nonces(owner)).to.be.bignumber.equal('1');
  140. // invalid call does not revert for this token implementation
  141. await this.token.permit(
  142. this.data.message.owner,
  143. this.data.message.spender,
  144. this.data.message.value,
  145. this.data.message.deadline,
  146. this.signature.v,
  147. this.signature.r,
  148. this.signature.s,
  149. );
  150. expect(await this.token.nonces(owner)).to.be.bignumber.equal('1');
  151. // invalid call revert when called through the SafeERC20 library
  152. await expectRevertCustomError(
  153. this.mock.$safePermit(
  154. this.token.address,
  155. this.data.message.owner,
  156. this.data.message.spender,
  157. this.data.message.value,
  158. this.data.message.deadline,
  159. this.signature.v,
  160. this.signature.r,
  161. this.signature.s,
  162. ),
  163. 'SafeERC20FailedOperation',
  164. [this.token.address],
  165. );
  166. expect(await this.token.nonces(owner)).to.be.bignumber.equal('1');
  167. });
  168. it('revert on invalid signature', async function () {
  169. // signature that is not valid for owner
  170. const invalidSignature = {
  171. v: 27,
  172. r: '0x71753dc5ecb5b4bfc0e3bc530d79ce5988760ed3f3a234c86a5546491f540775',
  173. s: '0x0049cedee5aed990aabed5ad6a9f6e3c565b63379894b5fa8b512eb2b79e485d',
  174. };
  175. // invalid call does not revert for this token implementation
  176. await this.token.permit(
  177. this.data.message.owner,
  178. this.data.message.spender,
  179. this.data.message.value,
  180. this.data.message.deadline,
  181. invalidSignature.v,
  182. invalidSignature.r,
  183. invalidSignature.s,
  184. );
  185. // invalid call revert when called through the SafeERC20 library
  186. await expectRevertCustomError(
  187. this.mock.$safePermit(
  188. this.token.address,
  189. this.data.message.owner,
  190. this.data.message.spender,
  191. this.data.message.value,
  192. this.data.message.deadline,
  193. invalidSignature.v,
  194. invalidSignature.r,
  195. invalidSignature.s,
  196. ),
  197. 'SafeERC20FailedOperation',
  198. [this.token.address],
  199. );
  200. });
  201. });
  202. describe('with usdt approval beaviour', function () {
  203. const spender = hasNoCode;
  204. beforeEach(async function () {
  205. this.token = await ERC20ForceApproveMock.new(name, symbol);
  206. });
  207. describe('with initial approval', function () {
  208. beforeEach(async function () {
  209. await this.token.$_approve(this.mock.address, spender, 100);
  210. });
  211. it('safeIncreaseAllowance works', async function () {
  212. await this.mock.$safeIncreaseAllowance(this.token.address, spender, 10);
  213. expect(this.token.allowance(this.mock.address, spender, 90));
  214. });
  215. it('safeDecreaseAllowance works', async function () {
  216. await this.mock.$safeDecreaseAllowance(this.token.address, spender, 10);
  217. expect(this.token.allowance(this.mock.address, spender, 110));
  218. });
  219. it('forceApprove works', async function () {
  220. await this.mock.$forceApprove(this.token.address, spender, 200);
  221. expect(this.token.allowance(this.mock.address, spender, 200));
  222. });
  223. });
  224. });
  225. });
  226. function shouldOnlyRevertOnErrors([owner, receiver, spender]) {
  227. describe('transfers', function () {
  228. beforeEach(async function () {
  229. await this.token.$_mint(owner, 100);
  230. await this.token.$_mint(this.mock.address, 100);
  231. await this.token.approve(this.mock.address, constants.MAX_UINT256, { from: owner });
  232. });
  233. it("doesn't revert on transfer", async function () {
  234. const { tx } = await this.mock.$safeTransfer(this.token.address, receiver, 10);
  235. await expectEvent.inTransaction(tx, this.token, 'Transfer', {
  236. from: this.mock.address,
  237. to: receiver,
  238. value: '10',
  239. });
  240. });
  241. it("doesn't revert on transferFrom", async function () {
  242. const { tx } = await this.mock.$safeTransferFrom(this.token.address, owner, receiver, 10);
  243. await expectEvent.inTransaction(tx, this.token, 'Transfer', {
  244. from: owner,
  245. to: receiver,
  246. value: '10',
  247. });
  248. });
  249. });
  250. describe('approvals', function () {
  251. context('with zero allowance', function () {
  252. beforeEach(async function () {
  253. await this.token.$_approve(this.mock.address, spender, 0);
  254. });
  255. it("doesn't revert when force approving a non-zero allowance", async function () {
  256. await this.mock.$forceApprove(this.token.address, spender, 100);
  257. expect(await this.token.allowance(this.mock.address, spender)).to.be.bignumber.equal('100');
  258. });
  259. it("doesn't revert when force approving a zero allowance", async function () {
  260. await this.mock.$forceApprove(this.token.address, spender, 0);
  261. expect(await this.token.allowance(this.mock.address, spender)).to.be.bignumber.equal('0');
  262. });
  263. it("doesn't revert when increasing the allowance", async function () {
  264. await this.mock.$safeIncreaseAllowance(this.token.address, spender, 10);
  265. expect(await this.token.allowance(this.mock.address, spender)).to.be.bignumber.equal('10');
  266. });
  267. it('reverts when decreasing the allowance', async function () {
  268. await expectRevertCustomError(
  269. this.mock.$safeDecreaseAllowance(this.token.address, spender, 10),
  270. 'SafeERC20FailedDecreaseAllowance',
  271. [spender, 0, 10],
  272. );
  273. });
  274. });
  275. context('with non-zero allowance', function () {
  276. beforeEach(async function () {
  277. await this.token.$_approve(this.mock.address, spender, 100);
  278. });
  279. it("doesn't revert when force approving a non-zero allowance", async function () {
  280. await this.mock.$forceApprove(this.token.address, spender, 20);
  281. expect(await this.token.allowance(this.mock.address, spender)).to.be.bignumber.equal('20');
  282. });
  283. it("doesn't revert when force approving a zero allowance", async function () {
  284. await this.mock.$forceApprove(this.token.address, spender, 0);
  285. expect(await this.token.allowance(this.mock.address, spender)).to.be.bignumber.equal('0');
  286. });
  287. it("doesn't revert when increasing the allowance", async function () {
  288. await this.mock.$safeIncreaseAllowance(this.token.address, spender, 10);
  289. expect(await this.token.allowance(this.mock.address, spender)).to.be.bignumber.equal('110');
  290. });
  291. it("doesn't revert when decreasing the allowance to a positive value", async function () {
  292. await this.mock.$safeDecreaseAllowance(this.token.address, spender, 50);
  293. expect(await this.token.allowance(this.mock.address, spender)).to.be.bignumber.equal('50');
  294. });
  295. it('reverts when decreasing the allowance to a negative value', async function () {
  296. await expectRevertCustomError(
  297. this.mock.$safeDecreaseAllowance(this.token.address, spender, 200),
  298. 'SafeERC20FailedDecreaseAllowance',
  299. [spender, 100, 200],
  300. );
  301. });
  302. });
  303. });
  304. }