SafeERC20.test.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463
  1. const { ethers } = require('hardhat');
  2. const { expect } = require('chai');
  3. const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
  4. const name = 'ERC20Mock';
  5. const symbol = 'ERC20Mock';
  6. const value = 100n;
  7. const data = '0x12345678';
  8. async function fixture() {
  9. const [hasNoCode, owner, receiver, spender, other] = await ethers.getSigners();
  10. const mock = await ethers.deployContract('$SafeERC20');
  11. const erc20ReturnFalseMock = await ethers.deployContract('$ERC20ReturnFalseMock', [name, symbol]);
  12. const erc20ReturnTrueMock = await ethers.deployContract('$ERC20', [name, symbol]); // default implementation returns true
  13. const erc20NoReturnMock = await ethers.deployContract('$ERC20NoReturnMock', [name, symbol]);
  14. const erc20ForceApproveMock = await ethers.deployContract('$ERC20ForceApproveMock', [name, symbol]);
  15. const erc1363Mock = await ethers.deployContract('$ERC1363', [name, symbol]);
  16. const erc1363ReturnFalseOnErc20Mock = await ethers.deployContract('$ERC1363ReturnFalseOnERC20Mock', [name, symbol]);
  17. const erc1363ReturnFalseMock = await ethers.deployContract('$ERC1363ReturnFalseMock', [name, symbol]);
  18. const erc1363NoReturnMock = await ethers.deployContract('$ERC1363NoReturnMock', [name, symbol]);
  19. const erc1363ForceApproveMock = await ethers.deployContract('$ERC1363ForceApproveMock', [name, symbol]);
  20. const erc1363Receiver = await ethers.deployContract('$ERC1363ReceiverMock');
  21. const erc1363Spender = await ethers.deployContract('$ERC1363SpenderMock');
  22. return {
  23. hasNoCode,
  24. owner,
  25. receiver,
  26. spender,
  27. other,
  28. mock,
  29. erc20ReturnFalseMock,
  30. erc20ReturnTrueMock,
  31. erc20NoReturnMock,
  32. erc20ForceApproveMock,
  33. erc1363Mock,
  34. erc1363ReturnFalseOnErc20Mock,
  35. erc1363ReturnFalseMock,
  36. erc1363NoReturnMock,
  37. erc1363ForceApproveMock,
  38. erc1363Receiver,
  39. erc1363Spender,
  40. };
  41. }
  42. describe('SafeERC20', function () {
  43. before(async function () {
  44. Object.assign(this, await loadFixture(fixture));
  45. });
  46. describe('with address that has no contract code', function () {
  47. beforeEach(async function () {
  48. this.token = this.hasNoCode;
  49. });
  50. it('reverts on transfer', async function () {
  51. await expect(this.mock.$safeTransfer(this.token, this.receiver, 0n))
  52. .to.be.revertedWithCustomError(this.mock, 'SafeERC20FailedOperation')
  53. .withArgs(this.token);
  54. });
  55. it('returns false on trySafeTransfer', async function () {
  56. await expect(this.mock.$trySafeTransfer(this.token, this.receiver, 0n))
  57. .to.emit(this.mock, 'return$trySafeTransfer')
  58. .withArgs(false);
  59. });
  60. it('reverts on transferFrom', async function () {
  61. await expect(this.mock.$safeTransferFrom(this.token, this.mock, this.receiver, 0n))
  62. .to.be.revertedWithCustomError(this.mock, 'SafeERC20FailedOperation')
  63. .withArgs(this.token);
  64. });
  65. it('returns false on trySafeTransferFrom', async function () {
  66. await expect(this.mock.$trySafeTransferFrom(this.token, this.mock, this.receiver, 0n))
  67. .to.emit(this.mock, 'return$trySafeTransferFrom')
  68. .withArgs(false);
  69. });
  70. it('reverts on increaseAllowance', async function () {
  71. // Call to 'token.allowance' does not return any data, resulting in a decoding error (revert without reason)
  72. await expect(this.mock.$safeIncreaseAllowance(this.token, this.spender, 0n)).to.be.revertedWithoutReason();
  73. });
  74. it('reverts on decreaseAllowance', async function () {
  75. // Call to 'token.allowance' does not return any data, resulting in a decoding error (revert without reason)
  76. await expect(this.mock.$safeDecreaseAllowance(this.token, this.spender, 0n)).to.be.revertedWithoutReason();
  77. });
  78. it('reverts on forceApprove', async function () {
  79. await expect(this.mock.$forceApprove(this.token, this.spender, 0n))
  80. .to.be.revertedWithCustomError(this.mock, 'SafeERC20FailedOperation')
  81. .withArgs(this.token);
  82. });
  83. });
  84. describe('with token that returns false on all calls', function () {
  85. beforeEach(async function () {
  86. this.token = this.erc20ReturnFalseMock;
  87. });
  88. it('reverts on transfer', async function () {
  89. await expect(this.mock.$safeTransfer(this.token, this.receiver, 0n))
  90. .to.be.revertedWithCustomError(this.mock, 'SafeERC20FailedOperation')
  91. .withArgs(this.token);
  92. });
  93. it('returns false on trySafeTransfer', async function () {
  94. await expect(this.mock.$trySafeTransfer(this.token, this.receiver, 0n))
  95. .to.emit(this.mock, 'return$trySafeTransfer')
  96. .withArgs(false);
  97. });
  98. it('reverts on transferFrom', async function () {
  99. await expect(this.mock.$safeTransferFrom(this.token, this.mock, this.receiver, 0n))
  100. .to.be.revertedWithCustomError(this.mock, 'SafeERC20FailedOperation')
  101. .withArgs(this.token);
  102. });
  103. it('returns false on trySafeTransferFrom', async function () {
  104. await expect(this.mock.$trySafeTransferFrom(this.token, this.mock, this.receiver, 0n))
  105. .to.emit(this.mock, 'return$trySafeTransferFrom')
  106. .withArgs(false);
  107. });
  108. it('reverts on increaseAllowance', async function () {
  109. await expect(this.mock.$safeIncreaseAllowance(this.token, this.spender, 0n))
  110. .to.be.revertedWithCustomError(this.mock, 'SafeERC20FailedOperation')
  111. .withArgs(this.token);
  112. });
  113. it('reverts on decreaseAllowance', async function () {
  114. await expect(this.mock.$safeDecreaseAllowance(this.token, this.spender, 0n))
  115. .to.be.revertedWithCustomError(this.mock, 'SafeERC20FailedOperation')
  116. .withArgs(this.token);
  117. });
  118. it('reverts on forceApprove', async function () {
  119. await expect(this.mock.$forceApprove(this.token, this.spender, 0n))
  120. .to.be.revertedWithCustomError(this.mock, 'SafeERC20FailedOperation')
  121. .withArgs(this.token);
  122. });
  123. });
  124. describe('with token that returns true on all calls', function () {
  125. beforeEach(async function () {
  126. this.token = this.erc20ReturnTrueMock;
  127. });
  128. shouldOnlyRevertOnErrors();
  129. });
  130. describe('with token that returns no boolean values', function () {
  131. beforeEach(async function () {
  132. this.token = this.erc20NoReturnMock;
  133. });
  134. shouldOnlyRevertOnErrors();
  135. });
  136. describe('with usdt approval behaviour', function () {
  137. beforeEach(async function () {
  138. this.token = this.erc20ForceApproveMock;
  139. });
  140. describe('with initial approval', function () {
  141. beforeEach(async function () {
  142. await this.token.$_approve(this.mock, this.spender, 100n);
  143. });
  144. it('safeIncreaseAllowance works', async function () {
  145. await this.mock.$safeIncreaseAllowance(this.token, this.spender, 10n);
  146. expect(await this.token.allowance(this.mock, this.spender)).to.equal(110n);
  147. });
  148. it('safeDecreaseAllowance works', async function () {
  149. await this.mock.$safeDecreaseAllowance(this.token, this.spender, 10n);
  150. expect(await this.token.allowance(this.mock, this.spender)).to.equal(90n);
  151. });
  152. it('forceApprove works', async function () {
  153. await this.mock.$forceApprove(this.token, this.spender, 200n);
  154. expect(await this.token.allowance(this.mock, this.spender)).to.equal(200n);
  155. });
  156. });
  157. });
  158. describe('with standard ERC1363', function () {
  159. beforeEach(async function () {
  160. this.token = this.erc1363Mock;
  161. });
  162. shouldOnlyRevertOnErrors();
  163. describe('transferAndCall', function () {
  164. it('cannot transferAndCall to an EOA directly', async function () {
  165. await this.token.$_mint(this.owner, 100n);
  166. await expect(this.token.connect(this.owner).transferAndCall(this.receiver, value, ethers.Typed.bytes(data)))
  167. .to.be.revertedWithCustomError(this.token, 'ERC1363InvalidReceiver')
  168. .withArgs(this.receiver);
  169. });
  170. it('can transferAndCall to an EOA using helper', async function () {
  171. await this.token.$_mint(this.mock, value);
  172. await expect(this.mock.$transferAndCallRelaxed(this.token, this.receiver, value, data))
  173. .to.emit(this.token, 'Transfer')
  174. .withArgs(this.mock, this.receiver, value);
  175. });
  176. it('can transferAndCall to an ERC1363Receiver using helper', async function () {
  177. await this.token.$_mint(this.mock, value);
  178. await expect(this.mock.$transferAndCallRelaxed(this.token, this.erc1363Receiver, value, data))
  179. .to.emit(this.token, 'Transfer')
  180. .withArgs(this.mock, this.erc1363Receiver, value)
  181. .to.emit(this.erc1363Receiver, 'Received')
  182. .withArgs(this.mock, this.mock, value, data);
  183. });
  184. });
  185. describe('transferFromAndCall', function () {
  186. it('can transferFromAndCall to an EOA using helper', async function () {
  187. await this.token.$_mint(this.owner, value);
  188. await this.token.$_approve(this.owner, this.mock, ethers.MaxUint256);
  189. await expect(this.mock.$transferFromAndCallRelaxed(this.token, this.owner, this.receiver, value, data))
  190. .to.emit(this.token, 'Transfer')
  191. .withArgs(this.owner, this.receiver, value);
  192. });
  193. it('can transferFromAndCall to an ERC1363Receiver using helper', async function () {
  194. await this.token.$_mint(this.owner, value);
  195. await this.token.$_approve(this.owner, this.mock, ethers.MaxUint256);
  196. await expect(this.mock.$transferFromAndCallRelaxed(this.token, this.owner, this.erc1363Receiver, value, data))
  197. .to.emit(this.token, 'Transfer')
  198. .withArgs(this.owner, this.erc1363Receiver, value)
  199. .to.emit(this.erc1363Receiver, 'Received')
  200. .withArgs(this.mock, this.owner, value, data);
  201. });
  202. });
  203. describe('approveAndCall', function () {
  204. it('can approveAndCall to an EOA using helper', async function () {
  205. await expect(this.mock.$approveAndCallRelaxed(this.token, this.receiver, value, data))
  206. .to.emit(this.token, 'Approval')
  207. .withArgs(this.mock, this.receiver, value);
  208. });
  209. it('can approveAndCall to an ERC1363Spender using helper', async function () {
  210. await expect(this.mock.$approveAndCallRelaxed(this.token, this.erc1363Spender, value, data))
  211. .to.emit(this.token, 'Approval')
  212. .withArgs(this.mock, this.erc1363Spender, value)
  213. .to.emit(this.erc1363Spender, 'Approved')
  214. .withArgs(this.mock, value, data);
  215. });
  216. });
  217. });
  218. describe('with ERC1363 that returns false on all ERC20 calls', function () {
  219. beforeEach(async function () {
  220. this.token = this.erc1363ReturnFalseOnErc20Mock;
  221. });
  222. it('reverts on transferAndCallRelaxed', async function () {
  223. await expect(this.mock.$transferAndCallRelaxed(this.token, this.erc1363Receiver, 0n, data))
  224. .to.be.revertedWithCustomError(this.token, 'ERC1363TransferFailed')
  225. .withArgs(this.erc1363Receiver, 0n);
  226. });
  227. it('reverts on transferFromAndCallRelaxed', async function () {
  228. await expect(this.mock.$transferFromAndCallRelaxed(this.token, this.mock, this.erc1363Receiver, 0n, data))
  229. .to.be.revertedWithCustomError(this.token, 'ERC1363TransferFromFailed')
  230. .withArgs(this.mock, this.erc1363Receiver, 0n);
  231. });
  232. it('reverts on approveAndCallRelaxed', async function () {
  233. await expect(this.mock.$approveAndCallRelaxed(this.token, this.erc1363Spender, 0n, data))
  234. .to.be.revertedWithCustomError(this.token, 'ERC1363ApproveFailed')
  235. .withArgs(this.erc1363Spender, 0n);
  236. });
  237. });
  238. describe('with ERC1363 that returns false on all ERC1363 calls', function () {
  239. beforeEach(async function () {
  240. this.token = this.erc1363ReturnFalseMock;
  241. });
  242. it('reverts on transferAndCallRelaxed', async function () {
  243. await expect(this.mock.$transferAndCallRelaxed(this.token, this.erc1363Receiver, 0n, data))
  244. .to.be.revertedWithCustomError(this.mock, 'SafeERC20FailedOperation')
  245. .withArgs(this.token);
  246. });
  247. it('reverts on transferFromAndCallRelaxed', async function () {
  248. await expect(this.mock.$transferFromAndCallRelaxed(this.token, this.mock, this.erc1363Receiver, 0n, data))
  249. .to.be.revertedWithCustomError(this.mock, 'SafeERC20FailedOperation')
  250. .withArgs(this.token);
  251. });
  252. it('reverts on approveAndCallRelaxed', async function () {
  253. await expect(this.mock.$approveAndCallRelaxed(this.token, this.erc1363Spender, 0n, data))
  254. .to.be.revertedWithCustomError(this.mock, 'SafeERC20FailedOperation')
  255. .withArgs(this.token);
  256. });
  257. });
  258. describe('with ERC1363 that returns no boolean values', function () {
  259. beforeEach(async function () {
  260. this.token = this.erc1363NoReturnMock;
  261. });
  262. it('reverts on transferAndCallRelaxed', async function () {
  263. await expect(
  264. this.mock.$transferAndCallRelaxed(this.token, this.erc1363Receiver, 0n, data),
  265. ).to.be.revertedWithoutReason();
  266. });
  267. it('reverts on transferFromAndCallRelaxed', async function () {
  268. await expect(
  269. this.mock.$transferFromAndCallRelaxed(this.token, this.mock, this.erc1363Receiver, 0n, data),
  270. ).to.be.revertedWithoutReason();
  271. });
  272. it('reverts on approveAndCallRelaxed', async function () {
  273. await expect(
  274. this.mock.$approveAndCallRelaxed(this.token, this.erc1363Spender, 0n, data),
  275. ).to.be.revertedWithoutReason();
  276. });
  277. });
  278. describe('with ERC1363 with usdt approval behaviour', function () {
  279. beforeEach(async function () {
  280. this.token = this.erc1363ForceApproveMock;
  281. });
  282. describe('without initial approval', function () {
  283. it('approveAndCallRelaxed works when recipient is an EOA', async function () {
  284. await this.mock.$approveAndCallRelaxed(this.token, this.spender, 10n, data);
  285. expect(await this.token.allowance(this.mock, this.spender)).to.equal(10n);
  286. });
  287. it('approveAndCallRelaxed works when recipient is a contract', async function () {
  288. await this.mock.$approveAndCallRelaxed(this.token, this.erc1363Spender, 10n, data);
  289. expect(await this.token.allowance(this.mock, this.erc1363Spender)).to.equal(10n);
  290. });
  291. });
  292. describe('with initial approval', function () {
  293. it('approveAndCallRelaxed works when recipient is an EOA', async function () {
  294. await this.token.$_approve(this.mock, this.spender, 100n);
  295. await this.mock.$approveAndCallRelaxed(this.token, this.spender, 10n, data);
  296. expect(await this.token.allowance(this.mock, this.spender)).to.equal(10n);
  297. });
  298. it('approveAndCallRelaxed reverts when recipient is a contract', async function () {
  299. await this.token.$_approve(this.mock, this.erc1363Spender, 100n);
  300. await expect(this.mock.$approveAndCallRelaxed(this.token, this.erc1363Spender, 10n, data)).to.be.revertedWith(
  301. 'USDT approval failure',
  302. );
  303. });
  304. });
  305. });
  306. });
  307. function shouldOnlyRevertOnErrors() {
  308. describe('transfers', function () {
  309. beforeEach(async function () {
  310. await this.token.$_mint(this.owner, 100n);
  311. await this.token.$_mint(this.mock, 100n);
  312. await this.token.$_approve(this.owner, this.mock, ethers.MaxUint256);
  313. });
  314. it("doesn't revert on transfer", async function () {
  315. await expect(this.mock.$safeTransfer(this.token, this.receiver, 10n))
  316. .to.emit(this.token, 'Transfer')
  317. .withArgs(this.mock, this.receiver, 10n);
  318. });
  319. it('returns true on trySafeTransfer', async function () {
  320. await expect(this.mock.$trySafeTransfer(this.token, this.receiver, 10n))
  321. .to.emit(this.mock, 'return$trySafeTransfer')
  322. .withArgs(true);
  323. });
  324. it("doesn't revert on transferFrom", async function () {
  325. await expect(this.mock.$safeTransferFrom(this.token, this.owner, this.receiver, 10n))
  326. .to.emit(this.token, 'Transfer')
  327. .withArgs(this.owner, this.receiver, 10n);
  328. });
  329. it('returns true on trySafeTransferFrom', async function () {
  330. await expect(this.mock.$trySafeTransferFrom(this.token, this.owner, this.receiver, 10n))
  331. .to.emit(this.mock, 'return$trySafeTransferFrom')
  332. .withArgs(true);
  333. });
  334. });
  335. describe('approvals', function () {
  336. describe('with zero allowance', function () {
  337. beforeEach(async function () {
  338. await this.token.$_approve(this.mock, this.spender, 0n);
  339. });
  340. it("doesn't revert when force approving a non-zero allowance", async function () {
  341. await this.mock.$forceApprove(this.token, this.spender, 100n);
  342. expect(await this.token.allowance(this.mock, this.spender)).to.equal(100n);
  343. });
  344. it("doesn't revert when force approving a zero allowance", async function () {
  345. await this.mock.$forceApprove(this.token, this.spender, 0n);
  346. expect(await this.token.allowance(this.mock, this.spender)).to.equal(0n);
  347. });
  348. it("doesn't revert when increasing the allowance", async function () {
  349. await this.mock.$safeIncreaseAllowance(this.token, this.spender, 10n);
  350. expect(await this.token.allowance(this.mock, this.spender)).to.equal(10n);
  351. });
  352. it('reverts when decreasing the allowance', async function () {
  353. await expect(this.mock.$safeDecreaseAllowance(this.token, this.spender, 10n))
  354. .to.be.revertedWithCustomError(this.mock, 'SafeERC20FailedDecreaseAllowance')
  355. .withArgs(this.spender, 0n, 10n);
  356. });
  357. });
  358. describe('with non-zero allowance', function () {
  359. beforeEach(async function () {
  360. await this.token.$_approve(this.mock, this.spender, 100n);
  361. });
  362. it("doesn't revert when force approving a non-zero allowance", async function () {
  363. await this.mock.$forceApprove(this.token, this.spender, 20n);
  364. expect(await this.token.allowance(this.mock, this.spender)).to.equal(20n);
  365. });
  366. it("doesn't revert when force approving a zero allowance", async function () {
  367. await this.mock.$forceApprove(this.token, this.spender, 0n);
  368. expect(await this.token.allowance(this.mock, this.spender)).to.equal(0n);
  369. });
  370. it("doesn't revert when increasing the allowance", async function () {
  371. await this.mock.$safeIncreaseAllowance(this.token, this.spender, 10n);
  372. expect(await this.token.allowance(this.mock, this.spender)).to.equal(110n);
  373. });
  374. it("doesn't revert when decreasing the allowance to a positive value", async function () {
  375. await this.mock.$safeDecreaseAllowance(this.token, this.spender, 50n);
  376. expect(await this.token.allowance(this.mock, this.spender)).to.equal(50n);
  377. });
  378. it('reverts when decreasing the allowance to a negative value', async function () {
  379. await expect(this.mock.$safeDecreaseAllowance(this.token, this.spender, 200n))
  380. .to.be.revertedWithCustomError(this.mock, 'SafeERC20FailedDecreaseAllowance')
  381. .withArgs(this.spender, 100n, 200n);
  382. });
  383. });
  384. });
  385. }