AccountERC7579.behavior.js 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563
  1. const { ethers, predeploy } = require('hardhat');
  2. const { expect } = require('chai');
  3. const { impersonate } = require('../../helpers/account');
  4. const { selector } = require('../../helpers/methods');
  5. const { zip } = require('../../helpers/iterate');
  6. const {
  7. encodeMode,
  8. encodeBatch,
  9. encodeSingle,
  10. encodeDelegate,
  11. MODULE_TYPE_VALIDATOR,
  12. MODULE_TYPE_EXECUTOR,
  13. MODULE_TYPE_FALLBACK,
  14. MODULE_TYPE_HOOK,
  15. CALL_TYPE_CALL,
  16. CALL_TYPE_BATCH,
  17. CALL_TYPE_DELEGATE,
  18. EXEC_TYPE_DEFAULT,
  19. EXEC_TYPE_TRY,
  20. } = require('../../helpers/erc7579');
  21. const CALL_TYPE_INVALID = '0x42';
  22. const EXEC_TYPE_INVALID = '0x17';
  23. const MODULE_TYPE_INVALID = 999n;
  24. const coder = ethers.AbiCoder.defaultAbiCoder();
  25. function shouldBehaveLikeAccountERC7579({ withHooks = false } = {}) {
  26. describe('AccountERC7579', function () {
  27. beforeEach(async function () {
  28. await this.mock.deploy();
  29. await this.other.sendTransaction({ to: this.mock.target, value: ethers.parseEther('1') });
  30. this.modules = {};
  31. this.modules[MODULE_TYPE_VALIDATOR] = await ethers.deployContract('$ERC7579ModuleMock', [MODULE_TYPE_VALIDATOR]);
  32. this.modules[MODULE_TYPE_EXECUTOR] = await ethers.deployContract('$ERC7579ModuleMock', [MODULE_TYPE_EXECUTOR]);
  33. this.modules[MODULE_TYPE_FALLBACK] = await ethers.deployContract('$ERC7579ModuleMock', [MODULE_TYPE_FALLBACK]);
  34. this.modules[MODULE_TYPE_HOOK] = await ethers.deployContract('$ERC7579HookMock');
  35. this.mockFromEntrypoint = this.mock.connect(await impersonate(predeploy.entrypoint.v08.target));
  36. this.mockFromExecutor = this.mock.connect(await impersonate(this.modules[MODULE_TYPE_EXECUTOR].target));
  37. });
  38. describe('accountId', function () {
  39. it('should return the account ID', async function () {
  40. await expect(this.mock.accountId()).to.eventually.equal(
  41. withHooks
  42. ? '@openzeppelin/community-contracts.AccountERC7579Hooked.v0.0.0'
  43. : '@openzeppelin/community-contracts.AccountERC7579.v0.0.0',
  44. );
  45. });
  46. });
  47. describe('supportsExecutionMode', function () {
  48. for (const [callType, execType] of zip(
  49. [CALL_TYPE_CALL, CALL_TYPE_BATCH, CALL_TYPE_DELEGATE, CALL_TYPE_INVALID],
  50. [EXEC_TYPE_DEFAULT, EXEC_TYPE_TRY, EXEC_TYPE_INVALID],
  51. )) {
  52. const result = callType != CALL_TYPE_INVALID && execType != EXEC_TYPE_INVALID;
  53. it(`${
  54. result ? 'does not support' : 'supports'
  55. } CALL_TYPE=${callType} and EXEC_TYPE=${execType} execution mode`, async function () {
  56. await expect(this.mock.supportsExecutionMode(encodeMode({ callType, execType }))).to.eventually.equal(result);
  57. });
  58. }
  59. });
  60. describe('supportsModule', function () {
  61. it('supports MODULE_TYPE_VALIDATOR module type', async function () {
  62. await expect(this.mock.supportsModule(MODULE_TYPE_VALIDATOR)).to.eventually.equal(true);
  63. });
  64. it('supports MODULE_TYPE_EXECUTOR module type', async function () {
  65. await expect(this.mock.supportsModule(MODULE_TYPE_EXECUTOR)).to.eventually.equal(true);
  66. });
  67. it('supports MODULE_TYPE_FALLBACK module type', async function () {
  68. await expect(this.mock.supportsModule(MODULE_TYPE_FALLBACK)).to.eventually.equal(true);
  69. });
  70. it(
  71. withHooks ? 'supports MODULE_TYPE_HOOK module type' : 'does not support MODULE_TYPE_HOOK module type',
  72. async function () {
  73. await expect(this.mock.supportsModule(MODULE_TYPE_HOOK)).to.eventually.equal(withHooks);
  74. },
  75. );
  76. it('does not support invalid module type', async function () {
  77. await expect(this.mock.supportsModule(MODULE_TYPE_INVALID)).to.eventually.equal(false);
  78. });
  79. });
  80. describe('module installation', function () {
  81. it('should revert if the caller is not the canonical entrypoint or the account itself', async function () {
  82. await expect(this.mock.connect(this.other).installModule(MODULE_TYPE_VALIDATOR, this.mock, '0x'))
  83. .to.be.revertedWithCustomError(this.mock, 'AccountUnauthorized')
  84. .withArgs(this.other);
  85. });
  86. it('should revert if the module type is not supported', async function () {
  87. await expect(this.mockFromEntrypoint.installModule(MODULE_TYPE_INVALID, this.mock, '0x'))
  88. .to.be.revertedWithCustomError(this.mock, 'ERC7579UnsupportedModuleType')
  89. .withArgs(MODULE_TYPE_INVALID);
  90. });
  91. it('should revert if the module is not the provided type', async function () {
  92. const instance = this.modules[MODULE_TYPE_EXECUTOR];
  93. await expect(this.mockFromEntrypoint.installModule(MODULE_TYPE_VALIDATOR, instance, '0x'))
  94. .to.be.revertedWithCustomError(this.mock, 'ERC7579MismatchedModuleTypeId')
  95. .withArgs(MODULE_TYPE_VALIDATOR, instance);
  96. });
  97. for (const moduleTypeId of [
  98. MODULE_TYPE_VALIDATOR,
  99. MODULE_TYPE_EXECUTOR,
  100. MODULE_TYPE_FALLBACK,
  101. withHooks && MODULE_TYPE_HOOK,
  102. ].filter(Boolean)) {
  103. const prefix = moduleTypeId == MODULE_TYPE_FALLBACK ? '0x12345678' : '0x';
  104. const initData = ethers.hexlify(ethers.randomBytes(256));
  105. const fullData = ethers.concat([prefix, initData]);
  106. it(`should install a module of type ${moduleTypeId}`, async function () {
  107. const instance = this.modules[moduleTypeId];
  108. await expect(this.mock.isModuleInstalled(moduleTypeId, instance, fullData)).to.eventually.equal(false);
  109. await expect(this.mockFromEntrypoint.installModule(moduleTypeId, instance, fullData))
  110. .to.emit(this.mock, 'ModuleInstalled')
  111. .withArgs(moduleTypeId, instance)
  112. .to.emit(instance, 'ModuleInstalledReceived')
  113. .withArgs(this.mock, initData); // After decoding MODULE_TYPE_FALLBACK, it should remove the fnSig
  114. await expect(this.mock.isModuleInstalled(moduleTypeId, instance, fullData)).to.eventually.equal(true);
  115. });
  116. it(`does not allow to install a module of ${moduleTypeId} id twice`, async function () {
  117. const instance = this.modules[moduleTypeId];
  118. await this.mockFromEntrypoint.installModule(moduleTypeId, instance, fullData);
  119. await expect(this.mock.isModuleInstalled(moduleTypeId, instance, fullData)).to.eventually.equal(true);
  120. await expect(this.mockFromEntrypoint.installModule(moduleTypeId, instance, fullData))
  121. .to.be.revertedWithCustomError(
  122. this.mock,
  123. moduleTypeId == MODULE_TYPE_HOOK ? 'ERC7579HookModuleAlreadyPresent' : 'ERC7579AlreadyInstalledModule',
  124. )
  125. .withArgs(...[moduleTypeId != MODULE_TYPE_HOOK && moduleTypeId, instance].filter(Boolean));
  126. });
  127. }
  128. withHooks &&
  129. describe('with hook', function () {
  130. beforeEach(async function () {
  131. await this.mockFromEntrypoint.$_installModule(MODULE_TYPE_HOOK, this.modules[MODULE_TYPE_HOOK], '0x');
  132. });
  133. it('should call the hook of the installed module when performing an module install', async function () {
  134. const instance = this.modules[MODULE_TYPE_EXECUTOR];
  135. const initData = ethers.hexlify(ethers.randomBytes(256));
  136. const precheckData = this.mock.interface.encodeFunctionData('installModule', [
  137. MODULE_TYPE_EXECUTOR,
  138. instance.target,
  139. initData,
  140. ]);
  141. await expect(this.mockFromEntrypoint.installModule(MODULE_TYPE_EXECUTOR, instance, initData))
  142. .to.emit(this.modules[MODULE_TYPE_HOOK], 'PreCheck')
  143. .withArgs(predeploy.entrypoint.v08, 0n, precheckData)
  144. .to.emit(this.modules[MODULE_TYPE_HOOK], 'PostCheck')
  145. .withArgs(precheckData);
  146. });
  147. });
  148. });
  149. describe('module uninstallation', function () {
  150. it('should revert if the caller is not the canonical entrypoint or the account itself', async function () {
  151. await expect(this.mock.connect(this.other).uninstallModule(MODULE_TYPE_VALIDATOR, this.mock, '0x'))
  152. .to.be.revertedWithCustomError(this.mock, 'AccountUnauthorized')
  153. .withArgs(this.other);
  154. });
  155. it('should revert if the module type is not supported', async function () {
  156. await expect(this.mockFromEntrypoint.uninstallModule(MODULE_TYPE_INVALID, this.mock, '0x'))
  157. .to.be.revertedWithCustomError(this.mock, 'ERC7579UnsupportedModuleType')
  158. .withArgs(MODULE_TYPE_INVALID);
  159. });
  160. for (const moduleTypeId of [
  161. MODULE_TYPE_VALIDATOR,
  162. MODULE_TYPE_EXECUTOR,
  163. MODULE_TYPE_FALLBACK,
  164. withHooks && MODULE_TYPE_HOOK,
  165. ].filter(Boolean)) {
  166. const prefix = moduleTypeId == MODULE_TYPE_FALLBACK ? '0x12345678' : '0x';
  167. const initData = ethers.hexlify(ethers.randomBytes(256));
  168. const fullData = ethers.concat([prefix, initData]);
  169. it(`should uninstall a module of type ${moduleTypeId}`, async function () {
  170. const instance = this.modules[moduleTypeId];
  171. await this.mock.$_installModule(moduleTypeId, instance, fullData);
  172. await expect(this.mock.isModuleInstalled(moduleTypeId, instance, fullData)).to.eventually.equal(true);
  173. await expect(this.mockFromEntrypoint.uninstallModule(moduleTypeId, instance, fullData))
  174. .to.emit(this.mock, 'ModuleUninstalled')
  175. .withArgs(moduleTypeId, instance)
  176. .to.emit(instance, 'ModuleUninstalledReceived')
  177. .withArgs(this.mock, initData); // After decoding MODULE_TYPE_FALLBACK, it should remove the fnSig
  178. await expect(this.mock.isModuleInstalled(moduleTypeId, instance, fullData)).to.eventually.equal(false);
  179. });
  180. it(`should revert uninstalling a module of type ${moduleTypeId} if it was not installed`, async function () {
  181. const instance = this.modules[moduleTypeId];
  182. await expect(this.mockFromEntrypoint.uninstallModule(moduleTypeId, instance, fullData))
  183. .to.be.revertedWithCustomError(this.mock, 'ERC7579UninstalledModule')
  184. .withArgs(moduleTypeId, instance);
  185. });
  186. }
  187. it('should revert uninstalling a module of type MODULE_TYPE_FALLBACK if a different module was installed for the provided selector', async function () {
  188. const instance = this.modules[MODULE_TYPE_FALLBACK];
  189. const anotherInstance = await ethers.deployContract('$ERC7579ModuleMock', [MODULE_TYPE_FALLBACK]);
  190. const initData = '0x12345678abcdef';
  191. await this.mockFromEntrypoint.$_installModule(MODULE_TYPE_FALLBACK, instance, initData);
  192. await expect(this.mockFromEntrypoint.uninstallModule(MODULE_TYPE_FALLBACK, anotherInstance, initData))
  193. .to.be.revertedWithCustomError(this.mock, 'ERC7579UninstalledModule')
  194. .withArgs(MODULE_TYPE_FALLBACK, anotherInstance);
  195. });
  196. withHooks &&
  197. describe('with hook', function () {
  198. beforeEach(async function () {
  199. await this.mockFromEntrypoint.$_installModule(MODULE_TYPE_HOOK, this.modules[MODULE_TYPE_HOOK], '0x');
  200. });
  201. it('should call the hook of the installed module when performing a module uninstall', async function () {
  202. const instance = this.modules[MODULE_TYPE_EXECUTOR];
  203. const initData = ethers.hexlify(ethers.randomBytes(256));
  204. const precheckData = this.mock.interface.encodeFunctionData('uninstallModule', [
  205. MODULE_TYPE_EXECUTOR,
  206. instance.target,
  207. initData,
  208. ]);
  209. await this.mock.$_installModule(MODULE_TYPE_EXECUTOR, instance, initData);
  210. await expect(this.mockFromEntrypoint.uninstallModule(MODULE_TYPE_EXECUTOR, instance, initData))
  211. .to.emit(this.modules[MODULE_TYPE_HOOK], 'PreCheck')
  212. .withArgs(predeploy.entrypoint.v08, 0n, precheckData)
  213. .to.emit(this.modules[MODULE_TYPE_HOOK], 'PostCheck')
  214. .withArgs(precheckData);
  215. });
  216. });
  217. });
  218. describe('execution', function () {
  219. beforeEach(async function () {
  220. await this.mock.$_installModule(MODULE_TYPE_EXECUTOR, this.modules[MODULE_TYPE_EXECUTOR], '0x');
  221. });
  222. for (const [execFn, mock] of [
  223. ['execute', 'mockFromEntrypoint'],
  224. ['executeFromExecutor', 'mockFromExecutor'],
  225. ]) {
  226. describe(`executing with ${execFn}`, function () {
  227. it('should revert if the call type is not supported', async function () {
  228. await expect(
  229. this[mock][execFn](encodeMode({ callType: CALL_TYPE_INVALID }), encodeSingle(this.other, 0, '0x')),
  230. )
  231. .to.be.revertedWithCustomError(this.mock, 'ERC7579UnsupportedCallType')
  232. .withArgs(ethers.solidityPacked(['bytes1'], [CALL_TYPE_INVALID]));
  233. });
  234. it('should revert if the caller is not authorized / installed', async function () {
  235. const error = execFn == 'execute' ? 'AccountUnauthorized' : 'ERC7579UninstalledModule';
  236. const args = execFn == 'execute' ? [this.other] : [MODULE_TYPE_EXECUTOR, this.other];
  237. await expect(
  238. this[mock]
  239. .connect(this.other)
  240. [execFn](encodeMode({ callType: CALL_TYPE_CALL }), encodeSingle(this.other, 0, '0x')),
  241. )
  242. .to.be.revertedWithCustomError(this.mock, error)
  243. .withArgs(...args);
  244. });
  245. describe('single execution', function () {
  246. it('calls the target with value and args', async function () {
  247. const value = 0x432;
  248. const data = encodeSingle(
  249. this.target,
  250. value,
  251. this.target.interface.encodeFunctionData('mockFunctionWithArgs', [42, '0x1234']),
  252. );
  253. const tx = this[mock][execFn](encodeMode({ callType: CALL_TYPE_CALL }), data);
  254. await expect(tx).to.emit(this.target, 'MockFunctionCalledWithArgs').withArgs(42, '0x1234');
  255. await expect(tx).to.changeEtherBalances([this.mock, this.target], [-value, value]);
  256. });
  257. it('reverts when target reverts in default ExecType', async function () {
  258. const value = 0x012;
  259. const data = encodeSingle(
  260. this.target,
  261. value,
  262. this.target.interface.encodeFunctionData('mockFunctionRevertsReason'),
  263. );
  264. await expect(this[mock][execFn](encodeMode({ callType: CALL_TYPE_CALL }), data)).to.be.revertedWith(
  265. 'CallReceiverMock: reverting',
  266. );
  267. });
  268. it('emits ERC7579TryExecuteFail event when target reverts in try ExecType', async function () {
  269. const value = 0x012;
  270. const data = encodeSingle(
  271. this.target,
  272. value,
  273. this.target.interface.encodeFunctionData('mockFunctionRevertsReason'),
  274. );
  275. await expect(this[mock][execFn](encodeMode({ callType: CALL_TYPE_CALL, execType: EXEC_TYPE_TRY }), data))
  276. .to.emit(this.mock, 'ERC7579TryExecuteFail')
  277. .withArgs(
  278. CALL_TYPE_CALL,
  279. ethers.solidityPacked(
  280. ['bytes4', 'bytes'],
  281. [selector('Error(string)'), coder.encode(['string'], ['CallReceiverMock: reverting'])],
  282. ),
  283. );
  284. });
  285. });
  286. describe('batch execution', function () {
  287. it('calls the targets with value and args', async function () {
  288. const value1 = 0x012;
  289. const value2 = 0x234;
  290. const data = encodeBatch(
  291. [this.target, value1, this.target.interface.encodeFunctionData('mockFunctionWithArgs', [42, '0x1234'])],
  292. [
  293. this.anotherTarget,
  294. value2,
  295. this.anotherTarget.interface.encodeFunctionData('mockFunctionWithArgs', [42, '0x1234']),
  296. ],
  297. );
  298. const tx = this[mock][execFn](encodeMode({ callType: CALL_TYPE_BATCH }), data);
  299. await expect(tx)
  300. .to.emit(this.target, 'MockFunctionCalledWithArgs')
  301. .to.emit(this.anotherTarget, 'MockFunctionCalledWithArgs');
  302. await expect(tx).to.changeEtherBalances(
  303. [this.mock, this.target, this.anotherTarget],
  304. [-value1 - value2, value1, value2],
  305. );
  306. });
  307. it('reverts when any target reverts in default ExecType', async function () {
  308. const value1 = 0x012;
  309. const value2 = 0x234;
  310. const data = encodeBatch(
  311. [this.target, value1, this.target.interface.encodeFunctionData('mockFunction')],
  312. [
  313. this.anotherTarget,
  314. value2,
  315. this.anotherTarget.interface.encodeFunctionData('mockFunctionRevertsReason'),
  316. ],
  317. );
  318. await expect(this[mock][execFn](encodeMode({ callType: CALL_TYPE_BATCH }), data)).to.be.revertedWith(
  319. 'CallReceiverMock: reverting',
  320. );
  321. });
  322. it('emits ERC7579TryExecuteFail event when any target reverts in try ExecType', async function () {
  323. const value1 = 0x012;
  324. const value2 = 0x234;
  325. const data = encodeBatch(
  326. [this.target, value1, this.target.interface.encodeFunctionData('mockFunction')],
  327. [
  328. this.anotherTarget,
  329. value2,
  330. this.anotherTarget.interface.encodeFunctionData('mockFunctionRevertsReason'),
  331. ],
  332. );
  333. const tx = this[mock][execFn](encodeMode({ callType: CALL_TYPE_BATCH, execType: EXEC_TYPE_TRY }), data);
  334. await expect(tx)
  335. .to.emit(this.mock, 'ERC7579TryExecuteFail')
  336. .withArgs(
  337. CALL_TYPE_BATCH,
  338. ethers.solidityPacked(
  339. ['bytes4', 'bytes'],
  340. [selector('Error(string)'), coder.encode(['string'], ['CallReceiverMock: reverting'])],
  341. ),
  342. );
  343. await expect(tx).to.changeEtherBalances(
  344. [this.mock, this.target, this.anotherTarget],
  345. [-value1, value1, 0],
  346. );
  347. });
  348. });
  349. describe('delegate call execution', function () {
  350. it('delegate calls the target', async function () {
  351. const slot = ethers.hexlify(ethers.randomBytes(32));
  352. const value = ethers.hexlify(ethers.randomBytes(32));
  353. const data = encodeDelegate(
  354. this.target,
  355. this.target.interface.encodeFunctionData('mockFunctionWritesStorage', [slot, value]),
  356. );
  357. await expect(ethers.provider.getStorage(this.mock.target, slot)).to.eventually.equal(ethers.ZeroHash);
  358. await this[mock][execFn](encodeMode({ callType: CALL_TYPE_DELEGATE }), data);
  359. await expect(ethers.provider.getStorage(this.mock.target, slot)).to.eventually.equal(value);
  360. });
  361. it('reverts when target reverts in default ExecType', async function () {
  362. const data = encodeDelegate(
  363. this.target,
  364. this.target.interface.encodeFunctionData('mockFunctionRevertsReason'),
  365. );
  366. await expect(this[mock][execFn](encodeMode({ callType: CALL_TYPE_DELEGATE }), data)).to.be.revertedWith(
  367. 'CallReceiverMock: reverting',
  368. );
  369. });
  370. it('emits ERC7579TryExecuteFail event when target reverts in try ExecType', async function () {
  371. const data = encodeDelegate(
  372. this.target,
  373. this.target.interface.encodeFunctionData('mockFunctionRevertsReason'),
  374. );
  375. await expect(
  376. this[mock][execFn](encodeMode({ callType: CALL_TYPE_DELEGATE, execType: EXEC_TYPE_TRY }), data),
  377. )
  378. .to.emit(this.mock, 'ERC7579TryExecuteFail')
  379. .withArgs(
  380. CALL_TYPE_CALL,
  381. ethers.solidityPacked(
  382. ['bytes4', 'bytes'],
  383. [selector('Error(string)'), coder.encode(['string'], ['CallReceiverMock: reverting'])],
  384. ),
  385. );
  386. });
  387. });
  388. withHooks &&
  389. describe('with hook', function () {
  390. beforeEach(async function () {
  391. await this.mockFromEntrypoint.$_installModule(MODULE_TYPE_HOOK, this.modules[MODULE_TYPE_HOOK], '0x');
  392. });
  393. it(`should call the hook of the installed module when executing ${execFn}`, async function () {
  394. const caller = execFn === 'execute' ? predeploy.entrypoint.v08 : this.modules[MODULE_TYPE_EXECUTOR];
  395. const value = 17;
  396. const data = this.target.interface.encodeFunctionData('mockFunctionWithArgs', [42, '0x1234']);
  397. const mode = encodeMode({ callType: CALL_TYPE_CALL });
  398. const call = encodeSingle(this.target, value, data);
  399. const precheckData = this[mock].interface.encodeFunctionData(execFn, [mode, call]);
  400. const tx = this[mock][execFn](mode, call, { value });
  401. await expect(tx)
  402. .to.emit(this.modules[MODULE_TYPE_HOOK], 'PreCheck')
  403. .withArgs(caller, value, precheckData)
  404. .to.emit(this.modules[MODULE_TYPE_HOOK], 'PostCheck')
  405. .withArgs(precheckData);
  406. await expect(tx).to.changeEtherBalances([caller, this.mock, this.target], [-value, 0n, value]);
  407. });
  408. });
  409. });
  410. }
  411. });
  412. describe('fallback', function () {
  413. beforeEach(async function () {
  414. this.fallbackHandler = await ethers.deployContract('$ERC7579FallbackHandlerMock');
  415. });
  416. it('reverts if there is no fallback module installed', async function () {
  417. const { selector } = this.fallbackHandler.callPayable.getFragment();
  418. await expect(this.fallbackHandler.attach(this.mock).callPayable())
  419. .to.be.revertedWithCustomError(this.mock, 'ERC7579MissingFallbackHandler')
  420. .withArgs(selector);
  421. });
  422. describe('with a fallback module installed', function () {
  423. beforeEach(async function () {
  424. await Promise.all(
  425. [
  426. this.fallbackHandler.callPayable.getFragment().selector,
  427. this.fallbackHandler.callView.getFragment().selector,
  428. this.fallbackHandler.callRevert.getFragment().selector,
  429. ].map(selector =>
  430. this.mock.$_installModule(
  431. MODULE_TYPE_FALLBACK,
  432. this.fallbackHandler,
  433. coder.encode(['bytes4', 'bytes'], [selector, '0x']),
  434. ),
  435. ),
  436. );
  437. });
  438. it('forwards the call to the fallback handler', async function () {
  439. const calldata = this.fallbackHandler.interface.encodeFunctionData('callPayable');
  440. const value = 17n;
  441. await expect(this.fallbackHandler.attach(this.mock).connect(this.other).callPayable({ value }))
  442. .to.emit(this.fallbackHandler, 'ERC7579FallbackHandlerMockCalled')
  443. .withArgs(this.mock, this.other, value, calldata);
  444. });
  445. it('returns answer from the fallback handler', async function () {
  446. await expect(this.fallbackHandler.attach(this.mock).connect(this.other).callView()).to.eventually.deep.equal([
  447. this.mock.target,
  448. this.other.address,
  449. ]);
  450. });
  451. it('bubble up reverts from the fallback handler', async function () {
  452. await expect(
  453. this.fallbackHandler.attach(this.mock).connect(this.other).callRevert(),
  454. ).to.be.revertedWithCustomError(this.fallbackHandler, 'ERC7579FallbackHandlerMockRevert');
  455. });
  456. withHooks &&
  457. describe('with hook', function () {
  458. beforeEach(async function () {
  459. await this.mockFromEntrypoint.$_installModule(MODULE_TYPE_HOOK, this.modules[MODULE_TYPE_HOOK], '0x');
  460. });
  461. it('should call the hook of the installed module when performing a callback', async function () {
  462. const precheckData = this.fallbackHandler.interface.encodeFunctionData('callPayable');
  463. const value = 17n;
  464. // call with interface: decode returned data
  465. await expect(this.fallbackHandler.attach(this.mock).connect(this.other).callPayable({ value }))
  466. .to.emit(this.modules[MODULE_TYPE_HOOK], 'PreCheck')
  467. .withArgs(this.other, value, precheckData)
  468. .to.emit(this.modules[MODULE_TYPE_HOOK], 'PostCheck')
  469. .withArgs(precheckData);
  470. });
  471. });
  472. });
  473. });
  474. });
  475. }
  476. module.exports = {
  477. shouldBehaveLikeAccountERC7579,
  478. };