SafeERC20.test.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363
  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. // Call to 'token.allowance' does not return any data, resulting in a decoding error (revert without reason)
  38. await expectRevert.unspecified(this.mock.$safeIncreaseAllowance(this.token.address, spender, 0));
  39. });
  40. it('reverts on decreaseAllowance', async function () {
  41. // Call to 'token.allowance' does not return any data, resulting in a decoding error (revert without 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. await expectRevertCustomError(
  70. this.mock.$safeIncreaseAllowance(this.token.address, spender, 0),
  71. 'SafeERC20FailedOperation',
  72. [this.token.address],
  73. );
  74. });
  75. it('reverts on decreaseAllowance', async function () {
  76. await expectRevertCustomError(
  77. this.mock.$safeDecreaseAllowance(this.token.address, spender, 0),
  78. 'SafeERC20FailedOperation',
  79. [this.token.address],
  80. );
  81. });
  82. it('reverts on forceApprove', async function () {
  83. await expectRevertCustomError(
  84. this.mock.$forceApprove(this.token.address, spender, 0),
  85. 'SafeERC20FailedOperation',
  86. [this.token.address],
  87. );
  88. });
  89. });
  90. describe('with token that returns true on all calls', function () {
  91. beforeEach(async function () {
  92. this.token = await ERC20ReturnTrueMock.new(name, symbol);
  93. });
  94. shouldOnlyRevertOnErrors(accounts);
  95. });
  96. describe('with token that returns no boolean values', function () {
  97. beforeEach(async function () {
  98. this.token = await ERC20NoReturnMock.new(name, symbol);
  99. });
  100. shouldOnlyRevertOnErrors(accounts);
  101. });
  102. describe("with token that doesn't revert on invalid permit", function () {
  103. const wallet = Wallet.generate();
  104. const owner = wallet.getAddressString();
  105. const spender = hasNoCode;
  106. beforeEach(async function () {
  107. this.token = await ERC20PermitNoRevertMock.new(name, symbol, name);
  108. this.data = await getDomain(this.token).then(domain => ({
  109. primaryType: 'Permit',
  110. types: { EIP712Domain: domainType(domain), Permit },
  111. domain,
  112. message: { owner, spender, value: '42', nonce: '0', deadline: constants.MAX_UINT256 },
  113. }));
  114. this.signature = fromRpcSig(ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data: this.data }));
  115. });
  116. it('accepts owner signature', async function () {
  117. expect(await this.token.nonces(owner)).to.be.bignumber.equal('0');
  118. expect(await this.token.allowance(owner, spender)).to.be.bignumber.equal('0');
  119. await this.mock.$safePermit(
  120. this.token.address,
  121. this.data.message.owner,
  122. this.data.message.spender,
  123. this.data.message.value,
  124. this.data.message.deadline,
  125. this.signature.v,
  126. this.signature.r,
  127. this.signature.s,
  128. );
  129. expect(await this.token.nonces(owner)).to.be.bignumber.equal('1');
  130. expect(await this.token.allowance(owner, spender)).to.be.bignumber.equal(this.data.message.value);
  131. });
  132. it('revert on reused signature', async function () {
  133. expect(await this.token.nonces(owner)).to.be.bignumber.equal('0');
  134. // use valid signature and consume nounce
  135. await this.mock.$safePermit(
  136. this.token.address,
  137. this.data.message.owner,
  138. this.data.message.spender,
  139. this.data.message.value,
  140. this.data.message.deadline,
  141. this.signature.v,
  142. this.signature.r,
  143. this.signature.s,
  144. );
  145. expect(await this.token.nonces(owner)).to.be.bignumber.equal('1');
  146. // invalid call does not revert for this token implementation
  147. await this.token.permit(
  148. this.data.message.owner,
  149. this.data.message.spender,
  150. this.data.message.value,
  151. this.data.message.deadline,
  152. this.signature.v,
  153. this.signature.r,
  154. this.signature.s,
  155. );
  156. expect(await this.token.nonces(owner)).to.be.bignumber.equal('1');
  157. // invalid call revert when called through the SafeERC20 library
  158. await expectRevertCustomError(
  159. this.mock.$safePermit(
  160. this.token.address,
  161. this.data.message.owner,
  162. this.data.message.spender,
  163. this.data.message.value,
  164. this.data.message.deadline,
  165. this.signature.v,
  166. this.signature.r,
  167. this.signature.s,
  168. ),
  169. 'SafeERC20FailedOperation',
  170. [this.token.address],
  171. );
  172. expect(await this.token.nonces(owner)).to.be.bignumber.equal('1');
  173. });
  174. it('revert on invalid signature', async function () {
  175. // signature that is not valid for owner
  176. const invalidSignature = {
  177. v: 27,
  178. r: '0x71753dc5ecb5b4bfc0e3bc530d79ce5988760ed3f3a234c86a5546491f540775',
  179. s: '0x0049cedee5aed990aabed5ad6a9f6e3c565b63379894b5fa8b512eb2b79e485d',
  180. };
  181. // invalid call does not revert for this token implementation
  182. await this.token.permit(
  183. this.data.message.owner,
  184. this.data.message.spender,
  185. this.data.message.value,
  186. this.data.message.deadline,
  187. invalidSignature.v,
  188. invalidSignature.r,
  189. invalidSignature.s,
  190. );
  191. // invalid call revert when called through the SafeERC20 library
  192. await expectRevertCustomError(
  193. this.mock.$safePermit(
  194. this.token.address,
  195. this.data.message.owner,
  196. this.data.message.spender,
  197. this.data.message.value,
  198. this.data.message.deadline,
  199. invalidSignature.v,
  200. invalidSignature.r,
  201. invalidSignature.s,
  202. ),
  203. 'SafeERC20FailedOperation',
  204. [this.token.address],
  205. );
  206. });
  207. });
  208. describe('with usdt approval beaviour', function () {
  209. const spender = hasNoCode;
  210. beforeEach(async function () {
  211. this.token = await ERC20ForceApproveMock.new(name, symbol);
  212. });
  213. describe('with initial approval', function () {
  214. beforeEach(async function () {
  215. await this.token.$_approve(this.mock.address, spender, 100);
  216. });
  217. it('safeIncreaseAllowance works', async function () {
  218. await this.mock.$safeIncreaseAllowance(this.token.address, spender, 10);
  219. expect(this.token.allowance(this.mock.address, spender, 90));
  220. });
  221. it('safeDecreaseAllowance works', async function () {
  222. await this.mock.$safeDecreaseAllowance(this.token.address, spender, 10);
  223. expect(this.token.allowance(this.mock.address, spender, 110));
  224. });
  225. it('forceApprove works', async function () {
  226. await this.mock.$forceApprove(this.token.address, spender, 200);
  227. expect(this.token.allowance(this.mock.address, spender, 200));
  228. });
  229. });
  230. });
  231. });
  232. function shouldOnlyRevertOnErrors([owner, receiver, spender]) {
  233. describe('transfers', function () {
  234. beforeEach(async function () {
  235. await this.token.$_mint(owner, 100);
  236. await this.token.$_mint(this.mock.address, 100);
  237. await this.token.approve(this.mock.address, constants.MAX_UINT256, { from: owner });
  238. });
  239. it("doesn't revert on transfer", async function () {
  240. const { tx } = await this.mock.$safeTransfer(this.token.address, receiver, 10);
  241. await expectEvent.inTransaction(tx, this.token, 'Transfer', {
  242. from: this.mock.address,
  243. to: receiver,
  244. value: '10',
  245. });
  246. });
  247. it("doesn't revert on transferFrom", async function () {
  248. const { tx } = await this.mock.$safeTransferFrom(this.token.address, owner, receiver, 10);
  249. await expectEvent.inTransaction(tx, this.token, 'Transfer', {
  250. from: owner,
  251. to: receiver,
  252. value: '10',
  253. });
  254. });
  255. });
  256. describe('approvals', function () {
  257. context('with zero allowance', function () {
  258. beforeEach(async function () {
  259. await this.token.$_approve(this.mock.address, spender, 0);
  260. });
  261. it("doesn't revert when force approving a non-zero allowance", async function () {
  262. await this.mock.$forceApprove(this.token.address, spender, 100);
  263. expect(await this.token.allowance(this.mock.address, spender)).to.be.bignumber.equal('100');
  264. });
  265. it("doesn't revert when force approving a zero allowance", async function () {
  266. await this.mock.$forceApprove(this.token.address, spender, 0);
  267. expect(await this.token.allowance(this.mock.address, spender)).to.be.bignumber.equal('0');
  268. });
  269. it("doesn't revert when increasing the allowance", async function () {
  270. await this.mock.$safeIncreaseAllowance(this.token.address, spender, 10);
  271. expect(await this.token.allowance(this.mock.address, spender)).to.be.bignumber.equal('10');
  272. });
  273. it('reverts when decreasing the allowance', async function () {
  274. await expectRevertCustomError(
  275. this.mock.$safeDecreaseAllowance(this.token.address, spender, 10),
  276. 'SafeERC20FailedDecreaseAllowance',
  277. [spender, 0, 10],
  278. );
  279. });
  280. });
  281. context('with non-zero allowance', function () {
  282. beforeEach(async function () {
  283. await this.token.$_approve(this.mock.address, spender, 100);
  284. });
  285. it("doesn't revert when force approving a non-zero allowance", async function () {
  286. await this.mock.$forceApprove(this.token.address, spender, 20);
  287. expect(await this.token.allowance(this.mock.address, spender)).to.be.bignumber.equal('20');
  288. });
  289. it("doesn't revert when force approving a zero allowance", async function () {
  290. await this.mock.$forceApprove(this.token.address, spender, 0);
  291. expect(await this.token.allowance(this.mock.address, spender)).to.be.bignumber.equal('0');
  292. });
  293. it("doesn't revert when increasing the allowance", async function () {
  294. await this.mock.$safeIncreaseAllowance(this.token.address, spender, 10);
  295. expect(await this.token.allowance(this.mock.address, spender)).to.be.bignumber.equal('110');
  296. });
  297. it("doesn't revert when decreasing the allowance to a positive value", async function () {
  298. await this.mock.$safeDecreaseAllowance(this.token.address, spender, 50);
  299. expect(await this.token.allowance(this.mock.address, spender)).to.be.bignumber.equal('50');
  300. });
  301. it('reverts when decreasing the allowance to a negative value', async function () {
  302. await expectRevertCustomError(
  303. this.mock.$safeDecreaseAllowance(this.token.address, spender, 200),
  304. 'SafeERC20FailedDecreaseAllowance',
  305. [spender, 100, 200],
  306. );
  307. });
  308. });
  309. });
  310. }