SafeERC20.test.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427
  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, 'AddressEmptyCode')
  53. .withArgs(this.token);
  54. });
  55. it('reverts on transferFrom', async function () {
  56. await expect(this.mock.$safeTransferFrom(this.token, this.mock, this.receiver, 0n))
  57. .to.be.revertedWithCustomError(this.mock, 'AddressEmptyCode')
  58. .withArgs(this.token);
  59. });
  60. it('reverts on increaseAllowance', async function () {
  61. // Call to 'token.allowance' does not return any data, resulting in a decoding error (revert without reason)
  62. await expect(this.mock.$safeIncreaseAllowance(this.token, this.spender, 0n)).to.be.revertedWithoutReason();
  63. });
  64. it('reverts on decreaseAllowance', async function () {
  65. // Call to 'token.allowance' does not return any data, resulting in a decoding error (revert without reason)
  66. await expect(this.mock.$safeDecreaseAllowance(this.token, this.spender, 0n)).to.be.revertedWithoutReason();
  67. });
  68. it('reverts on forceApprove', async function () {
  69. await expect(this.mock.$forceApprove(this.token, this.spender, 0n))
  70. .to.be.revertedWithCustomError(this.mock, 'AddressEmptyCode')
  71. .withArgs(this.token);
  72. });
  73. });
  74. describe('with token that returns false on all calls', function () {
  75. beforeEach(async function () {
  76. this.token = this.erc20ReturnFalseMock;
  77. });
  78. it('reverts on transfer', async function () {
  79. await expect(this.mock.$safeTransfer(this.token, this.receiver, 0n))
  80. .to.be.revertedWithCustomError(this.mock, 'SafeERC20FailedOperation')
  81. .withArgs(this.token);
  82. });
  83. it('reverts on transferFrom', async function () {
  84. await expect(this.mock.$safeTransferFrom(this.token, this.mock, this.receiver, 0n))
  85. .to.be.revertedWithCustomError(this.mock, 'SafeERC20FailedOperation')
  86. .withArgs(this.token);
  87. });
  88. it('reverts on increaseAllowance', async function () {
  89. await expect(this.mock.$safeIncreaseAllowance(this.token, this.spender, 0n))
  90. .to.be.revertedWithCustomError(this.mock, 'SafeERC20FailedOperation')
  91. .withArgs(this.token);
  92. });
  93. it('reverts on decreaseAllowance', async function () {
  94. await expect(this.mock.$safeDecreaseAllowance(this.token, this.spender, 0n))
  95. .to.be.revertedWithCustomError(this.mock, 'SafeERC20FailedOperation')
  96. .withArgs(this.token);
  97. });
  98. it('reverts on forceApprove', async function () {
  99. await expect(this.mock.$forceApprove(this.token, this.spender, 0n))
  100. .to.be.revertedWithCustomError(this.mock, 'SafeERC20FailedOperation')
  101. .withArgs(this.token);
  102. });
  103. });
  104. describe('with token that returns true on all calls', function () {
  105. beforeEach(async function () {
  106. this.token = this.erc20ReturnTrueMock;
  107. });
  108. shouldOnlyRevertOnErrors();
  109. });
  110. describe('with token that returns no boolean values', function () {
  111. beforeEach(async function () {
  112. this.token = this.erc20NoReturnMock;
  113. });
  114. shouldOnlyRevertOnErrors();
  115. });
  116. describe('with usdt approval behaviour', function () {
  117. beforeEach(async function () {
  118. this.token = this.erc20ForceApproveMock;
  119. });
  120. describe('with initial approval', function () {
  121. beforeEach(async function () {
  122. await this.token.$_approve(this.mock, this.spender, 100n);
  123. });
  124. it('safeIncreaseAllowance works', async function () {
  125. await this.mock.$safeIncreaseAllowance(this.token, this.spender, 10n);
  126. expect(await this.token.allowance(this.mock, this.spender)).to.equal(110n);
  127. });
  128. it('safeDecreaseAllowance works', async function () {
  129. await this.mock.$safeDecreaseAllowance(this.token, this.spender, 10n);
  130. expect(await this.token.allowance(this.mock, this.spender)).to.equal(90n);
  131. });
  132. it('forceApprove works', async function () {
  133. await this.mock.$forceApprove(this.token, this.spender, 200n);
  134. expect(await this.token.allowance(this.mock, this.spender)).to.equal(200n);
  135. });
  136. });
  137. });
  138. describe('with standard ERC1363', function () {
  139. beforeEach(async function () {
  140. this.token = this.erc1363Mock;
  141. });
  142. shouldOnlyRevertOnErrors();
  143. describe('transferAndCall', function () {
  144. it('cannot transferAndCall to an EOA directly', async function () {
  145. await this.token.$_mint(this.owner, 100n);
  146. await expect(this.token.connect(this.owner).transferAndCall(this.receiver, value, ethers.Typed.bytes(data)))
  147. .to.be.revertedWithCustomError(this.token, 'ERC1363InvalidReceiver')
  148. .withArgs(this.receiver);
  149. });
  150. it('can transferAndCall to an EOA using helper', async function () {
  151. await this.token.$_mint(this.mock, value);
  152. await expect(this.mock.$transferAndCallRelaxed(this.token, this.receiver, value, data))
  153. .to.emit(this.token, 'Transfer')
  154. .withArgs(this.mock, this.receiver, value);
  155. });
  156. it('can transferAndCall to an ERC1363Receiver using helper', async function () {
  157. await this.token.$_mint(this.mock, value);
  158. await expect(this.mock.$transferAndCallRelaxed(this.token, this.erc1363Receiver, value, data))
  159. .to.emit(this.token, 'Transfer')
  160. .withArgs(this.mock, this.erc1363Receiver, value)
  161. .to.emit(this.erc1363Receiver, 'Received')
  162. .withArgs(this.mock, this.mock, value, data);
  163. });
  164. });
  165. describe('transferFromAndCall', function () {
  166. it('can transferFromAndCall to an EOA using helper', async function () {
  167. await this.token.$_mint(this.owner, value);
  168. await this.token.$_approve(this.owner, this.mock, ethers.MaxUint256);
  169. await expect(this.mock.$transferFromAndCallRelaxed(this.token, this.owner, this.receiver, value, data))
  170. .to.emit(this.token, 'Transfer')
  171. .withArgs(this.owner, this.receiver, value);
  172. });
  173. it('can transferFromAndCall to an ERC1363Receiver using helper', async function () {
  174. await this.token.$_mint(this.owner, value);
  175. await this.token.$_approve(this.owner, this.mock, ethers.MaxUint256);
  176. await expect(this.mock.$transferFromAndCallRelaxed(this.token, this.owner, this.erc1363Receiver, value, data))
  177. .to.emit(this.token, 'Transfer')
  178. .withArgs(this.owner, this.erc1363Receiver, value)
  179. .to.emit(this.erc1363Receiver, 'Received')
  180. .withArgs(this.mock, this.owner, value, data);
  181. });
  182. });
  183. describe('approveAndCall', function () {
  184. it('can approveAndCall to an EOA using helper', async function () {
  185. await expect(this.mock.$approveAndCallRelaxed(this.token, this.receiver, value, data))
  186. .to.emit(this.token, 'Approval')
  187. .withArgs(this.mock, this.receiver, value);
  188. });
  189. it('can approveAndCall to an ERC1363Spender using helper', async function () {
  190. await expect(this.mock.$approveAndCallRelaxed(this.token, this.erc1363Spender, value, data))
  191. .to.emit(this.token, 'Approval')
  192. .withArgs(this.mock, this.erc1363Spender, value)
  193. .to.emit(this.erc1363Spender, 'Approved')
  194. .withArgs(this.mock, value, data);
  195. });
  196. });
  197. });
  198. describe('with ERC1363 that returns false on all ERC20 calls', function () {
  199. beforeEach(async function () {
  200. this.token = this.erc1363ReturnFalseOnErc20Mock;
  201. });
  202. it('reverts on transferAndCallRelaxed', async function () {
  203. await expect(this.mock.$transferAndCallRelaxed(this.token, this.erc1363Receiver, 0n, data))
  204. .to.be.revertedWithCustomError(this.token, 'ERC1363TransferFailed')
  205. .withArgs(this.erc1363Receiver, 0n);
  206. });
  207. it('reverts on transferFromAndCallRelaxed', async function () {
  208. await expect(this.mock.$transferFromAndCallRelaxed(this.token, this.mock, this.erc1363Receiver, 0n, data))
  209. .to.be.revertedWithCustomError(this.token, 'ERC1363TransferFromFailed')
  210. .withArgs(this.mock, this.erc1363Receiver, 0n);
  211. });
  212. it('reverts on approveAndCallRelaxed', async function () {
  213. await expect(this.mock.$approveAndCallRelaxed(this.token, this.erc1363Spender, 0n, data))
  214. .to.be.revertedWithCustomError(this.token, 'ERC1363ApproveFailed')
  215. .withArgs(this.erc1363Spender, 0n);
  216. });
  217. });
  218. describe('with ERC1363 that returns false on all ERC1363 calls', function () {
  219. beforeEach(async function () {
  220. this.token = this.erc1363ReturnFalseMock;
  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.mock, 'SafeERC20FailedOperation')
  225. .withArgs(this.token);
  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.mock, 'SafeERC20FailedOperation')
  230. .withArgs(this.token);
  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.mock, 'SafeERC20FailedOperation')
  235. .withArgs(this.token);
  236. });
  237. });
  238. describe('with ERC1363 that returns no boolean values', function () {
  239. beforeEach(async function () {
  240. this.token = this.erc1363NoReturnMock;
  241. });
  242. it('reverts on transferAndCallRelaxed', async function () {
  243. await expect(
  244. this.mock.$transferAndCallRelaxed(this.token, this.erc1363Receiver, 0n, data),
  245. ).to.be.revertedWithoutReason();
  246. });
  247. it('reverts on transferFromAndCallRelaxed', async function () {
  248. await expect(
  249. this.mock.$transferFromAndCallRelaxed(this.token, this.mock, this.erc1363Receiver, 0n, data),
  250. ).to.be.revertedWithoutReason();
  251. });
  252. it('reverts on approveAndCallRelaxed', async function () {
  253. await expect(
  254. this.mock.$approveAndCallRelaxed(this.token, this.erc1363Spender, 0n, data),
  255. ).to.be.revertedWithoutReason();
  256. });
  257. });
  258. describe('with ERC1363 with usdt approval behaviour', function () {
  259. beforeEach(async function () {
  260. this.token = this.erc1363ForceApproveMock;
  261. });
  262. describe('without initial approval', function () {
  263. it('approveAndCallRelaxed works when recipient is an EOA', async function () {
  264. await this.mock.$approveAndCallRelaxed(this.token, this.spender, 10n, data);
  265. expect(await this.token.allowance(this.mock, this.spender)).to.equal(10n);
  266. });
  267. it('approveAndCallRelaxed works when recipient is a contract', async function () {
  268. await this.mock.$approveAndCallRelaxed(this.token, this.erc1363Spender, 10n, data);
  269. expect(await this.token.allowance(this.mock, this.erc1363Spender)).to.equal(10n);
  270. });
  271. });
  272. describe('with initial approval', function () {
  273. it('approveAndCallRelaxed works when recipient is an EOA', async function () {
  274. await this.token.$_approve(this.mock, this.spender, 100n);
  275. await this.mock.$approveAndCallRelaxed(this.token, this.spender, 10n, data);
  276. expect(await this.token.allowance(this.mock, this.spender)).to.equal(10n);
  277. });
  278. it('approveAndCallRelaxed reverts when recipient is a contract', async function () {
  279. await this.token.$_approve(this.mock, this.erc1363Spender, 100n);
  280. await expect(this.mock.$approveAndCallRelaxed(this.token, this.erc1363Spender, 10n, data)).to.be.revertedWith(
  281. 'USDT approval failure',
  282. );
  283. });
  284. });
  285. });
  286. });
  287. function shouldOnlyRevertOnErrors() {
  288. describe('transfers', function () {
  289. beforeEach(async function () {
  290. await this.token.$_mint(this.owner, 100n);
  291. await this.token.$_mint(this.mock, 100n);
  292. await this.token.$_approve(this.owner, this.mock, ethers.MaxUint256);
  293. });
  294. it("doesn't revert on transfer", async function () {
  295. await expect(this.mock.$safeTransfer(this.token, this.receiver, 10n))
  296. .to.emit(this.token, 'Transfer')
  297. .withArgs(this.mock, this.receiver, 10n);
  298. });
  299. it("doesn't revert on transferFrom", async function () {
  300. await expect(this.mock.$safeTransferFrom(this.token, this.owner, this.receiver, 10n))
  301. .to.emit(this.token, 'Transfer')
  302. .withArgs(this.owner, this.receiver, 10n);
  303. });
  304. });
  305. describe('approvals', function () {
  306. describe('with zero allowance', function () {
  307. beforeEach(async function () {
  308. await this.token.$_approve(this.mock, this.spender, 0n);
  309. });
  310. it("doesn't revert when force approving a non-zero allowance", async function () {
  311. await this.mock.$forceApprove(this.token, this.spender, 100n);
  312. expect(await this.token.allowance(this.mock, this.spender)).to.equal(100n);
  313. });
  314. it("doesn't revert when force approving a zero allowance", async function () {
  315. await this.mock.$forceApprove(this.token, this.spender, 0n);
  316. expect(await this.token.allowance(this.mock, this.spender)).to.equal(0n);
  317. });
  318. it("doesn't revert when increasing the allowance", async function () {
  319. await this.mock.$safeIncreaseAllowance(this.token, this.spender, 10n);
  320. expect(await this.token.allowance(this.mock, this.spender)).to.equal(10n);
  321. });
  322. it('reverts when decreasing the allowance', async function () {
  323. await expect(this.mock.$safeDecreaseAllowance(this.token, this.spender, 10n))
  324. .to.be.revertedWithCustomError(this.mock, 'SafeERC20FailedDecreaseAllowance')
  325. .withArgs(this.spender, 0n, 10n);
  326. });
  327. });
  328. describe('with non-zero allowance', function () {
  329. beforeEach(async function () {
  330. await this.token.$_approve(this.mock, this.spender, 100n);
  331. });
  332. it("doesn't revert when force approving a non-zero allowance", async function () {
  333. await this.mock.$forceApprove(this.token, this.spender, 20n);
  334. expect(await this.token.allowance(this.mock, this.spender)).to.equal(20n);
  335. });
  336. it("doesn't revert when force approving a zero allowance", async function () {
  337. await this.mock.$forceApprove(this.token, this.spender, 0n);
  338. expect(await this.token.allowance(this.mock, this.spender)).to.equal(0n);
  339. });
  340. it("doesn't revert when increasing the allowance", async function () {
  341. await this.mock.$safeIncreaseAllowance(this.token, this.spender, 10n);
  342. expect(await this.token.allowance(this.mock, this.spender)).to.equal(110n);
  343. });
  344. it("doesn't revert when decreasing the allowance to a positive value", async function () {
  345. await this.mock.$safeDecreaseAllowance(this.token, this.spender, 50n);
  346. expect(await this.token.allowance(this.mock, this.spender)).to.equal(50n);
  347. });
  348. it('reverts when decreasing the allowance to a negative value', async function () {
  349. await expect(this.mock.$safeDecreaseAllowance(this.token, this.spender, 200n))
  350. .to.be.revertedWithCustomError(this.mock, 'SafeERC20FailedDecreaseAllowance')
  351. .withArgs(this.spender, 100n, 200n);
  352. });
  353. });
  354. });
  355. }