draft-ERC7579Utils.test.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399
  1. const { ethers } = require('hardhat');
  2. const { expect } = require('chai');
  3. const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
  4. const {
  5. CALL_TYPE_CALL,
  6. CALL_TYPE_BATCH,
  7. CALL_TYPE_DELEGATE,
  8. EXEC_TYPE_DEFAULT,
  9. EXEC_TYPE_TRY,
  10. encodeSingle,
  11. encodeBatch,
  12. encodeDelegate,
  13. encodeMode,
  14. } = require('../../helpers/erc7579');
  15. const { selector } = require('../../helpers/methods');
  16. const coder = ethers.AbiCoder.defaultAbiCoder();
  17. const fixture = async () => {
  18. const [sender] = await ethers.getSigners();
  19. const utils = await ethers.deployContract('$ERC7579Utils', { value: ethers.parseEther('1') });
  20. const utilsGlobal = await ethers.deployContract('$ERC7579UtilsGlobalMock');
  21. const target = await ethers.deployContract('CallReceiverMock');
  22. const anotherTarget = await ethers.deployContract('CallReceiverMock');
  23. return { utils, utilsGlobal, target, anotherTarget, sender };
  24. };
  25. describe('ERC7579Utils', function () {
  26. beforeEach(async function () {
  27. Object.assign(this, await loadFixture(fixture));
  28. });
  29. it('constants', async function () {
  30. await expect(this.utils.$CALLTYPE_SINGLE()).to.eventually.equal(CALL_TYPE_CALL);
  31. await expect(this.utils.$CALLTYPE_BATCH()).to.eventually.equal(CALL_TYPE_BATCH);
  32. await expect(this.utils.$CALLTYPE_DELEGATECALL()).to.eventually.equal(CALL_TYPE_DELEGATE);
  33. await expect(this.utils.$EXECTYPE_DEFAULT()).to.eventually.equal(EXEC_TYPE_DEFAULT);
  34. await expect(this.utils.$EXECTYPE_TRY()).to.eventually.equal(EXEC_TYPE_TRY);
  35. });
  36. describe('execSingle', function () {
  37. it('calls the target with value', async function () {
  38. const value = 0x012;
  39. const data = encodeSingle(this.target, value, this.target.interface.encodeFunctionData('mockFunction'));
  40. await expect(this.utils.$execSingle(data, EXEC_TYPE_DEFAULT)).to.emit(this.target, 'MockFunctionCalled');
  41. await expect(ethers.provider.getBalance(this.target)).to.eventually.equal(value);
  42. });
  43. it('calls the target with value and args', async function () {
  44. const value = 0x432;
  45. const data = encodeSingle(
  46. this.target,
  47. value,
  48. this.target.interface.encodeFunctionData('mockFunctionWithArgs', [42, '0x1234']),
  49. );
  50. await expect(this.utils.$execSingle(data, EXEC_TYPE_DEFAULT))
  51. .to.emit(this.target, 'MockFunctionCalledWithArgs')
  52. .withArgs(42, '0x1234');
  53. await expect(ethers.provider.getBalance(this.target)).to.eventually.equal(value);
  54. });
  55. it('default to calling self is target is address(0) (ERC-7821 calldata compression)', async function () {
  56. const data = encodeSingle(
  57. ethers.ZeroAddress, // address(0)
  58. 0,
  59. this.utils.interface.encodeFunctionData('$CALLTYPE_SINGLE', []),
  60. );
  61. await expect(this.utils.$execSingle(data, EXEC_TYPE_DEFAULT))
  62. .to.emit(this.utils, 'return$execSingle')
  63. .withArgs([ethers.zeroPadBytes(CALL_TYPE_CALL, 32)]);
  64. });
  65. it('reverts when target reverts in default ExecType', async function () {
  66. const value = 0x012;
  67. const data = encodeSingle(
  68. this.target,
  69. value,
  70. this.target.interface.encodeFunctionData('mockFunctionRevertsReason'),
  71. );
  72. await expect(this.utils.$execSingle(data, EXEC_TYPE_DEFAULT)).to.be.revertedWith('CallReceiverMock: reverting');
  73. });
  74. it('emits ERC7579TryExecuteFail event when target reverts in try ExecType', async function () {
  75. const value = 0x012;
  76. const data = encodeSingle(
  77. this.target,
  78. value,
  79. this.target.interface.encodeFunctionData('mockFunctionRevertsReason'),
  80. );
  81. await expect(this.utils.$execSingle(data, EXEC_TYPE_TRY))
  82. .to.emit(this.utils, 'ERC7579TryExecuteFail')
  83. .withArgs(
  84. CALL_TYPE_CALL,
  85. ethers.solidityPacked(
  86. ['bytes4', 'bytes'],
  87. [selector('Error(string)'), coder.encode(['string'], ['CallReceiverMock: reverting'])],
  88. ),
  89. );
  90. });
  91. it('reverts with an invalid exec type', async function () {
  92. const value = 0x012;
  93. const data = encodeSingle(this.target, value, this.target.interface.encodeFunctionData('mockFunction'));
  94. await expect(this.utils.$execSingle(data, '0x03'))
  95. .to.be.revertedWithCustomError(this.utils, 'ERC7579UnsupportedExecType')
  96. .withArgs('0x03');
  97. });
  98. });
  99. describe('execBatch', function () {
  100. it('calls the targets with value', async function () {
  101. const value1 = 0x012;
  102. const value2 = 0x234;
  103. const data = encodeBatch(
  104. [this.target, value1, this.target.interface.encodeFunctionData('mockFunction')],
  105. [this.anotherTarget, value2, this.anotherTarget.interface.encodeFunctionData('mockFunction')],
  106. );
  107. await expect(this.utils.$execBatch(data, EXEC_TYPE_DEFAULT))
  108. .to.emit(this.target, 'MockFunctionCalled')
  109. .to.emit(this.anotherTarget, 'MockFunctionCalled');
  110. await expect(ethers.provider.getBalance(this.target)).to.eventually.equal(value1);
  111. await expect(ethers.provider.getBalance(this.anotherTarget)).to.eventually.equal(value2);
  112. });
  113. it('calls the targets with value and args', async function () {
  114. const value1 = 0x012;
  115. const value2 = 0x234;
  116. const data = encodeBatch(
  117. [this.target, value1, this.target.interface.encodeFunctionData('mockFunctionWithArgs', [42, '0x1234'])],
  118. [
  119. this.anotherTarget,
  120. value2,
  121. this.anotherTarget.interface.encodeFunctionData('mockFunctionWithArgs', [42, '0x1234']),
  122. ],
  123. );
  124. await expect(this.utils.$execBatch(data, EXEC_TYPE_DEFAULT))
  125. .to.emit(this.target, 'MockFunctionCalledWithArgs')
  126. .to.emit(this.anotherTarget, 'MockFunctionCalledWithArgs');
  127. await expect(ethers.provider.getBalance(this.target)).to.eventually.equal(value1);
  128. await expect(ethers.provider.getBalance(this.anotherTarget)).to.eventually.equal(value2);
  129. });
  130. it('default to calling self is target is address(0) (ERC-7821 calldata compression)', async function () {
  131. const data = encodeBatch(
  132. [ethers.ZeroAddress, 0, this.utils.interface.encodeFunctionData('$CALLTYPE_SINGLE', [])],
  133. [ethers.ZeroAddress, 0, this.utils.interface.encodeFunctionData('$CALLTYPE_BATCH', [])],
  134. );
  135. await expect(this.utils.$execBatch(data, EXEC_TYPE_DEFAULT))
  136. .to.emit(this.utils, 'return$execBatch')
  137. .withArgs([ethers.zeroPadBytes(CALL_TYPE_CALL, 32), ethers.zeroPadBytes(CALL_TYPE_BATCH, 32)]);
  138. });
  139. it('reverts when any target reverts in default ExecType', async function () {
  140. const value1 = 0x012;
  141. const value2 = 0x234;
  142. const data = encodeBatch(
  143. [this.target, value1, this.target.interface.encodeFunctionData('mockFunction')],
  144. [this.anotherTarget, value2, this.anotherTarget.interface.encodeFunctionData('mockFunctionRevertsReason')],
  145. );
  146. await expect(this.utils.$execBatch(data, EXEC_TYPE_DEFAULT)).to.be.revertedWith('CallReceiverMock: reverting');
  147. });
  148. it('emits ERC7579TryExecuteFail event when any target reverts in try ExecType', async function () {
  149. const value1 = 0x012;
  150. const value2 = 0x234;
  151. const data = encodeBatch(
  152. [this.target, value1, this.target.interface.encodeFunctionData('mockFunction')],
  153. [this.anotherTarget, value2, this.anotherTarget.interface.encodeFunctionData('mockFunctionRevertsReason')],
  154. );
  155. await expect(this.utils.$execBatch(data, EXEC_TYPE_TRY))
  156. .to.emit(this.utils, 'ERC7579TryExecuteFail')
  157. .withArgs(
  158. CALL_TYPE_BATCH,
  159. ethers.solidityPacked(
  160. ['bytes4', 'bytes'],
  161. [selector('Error(string)'), coder.encode(['string'], ['CallReceiverMock: reverting'])],
  162. ),
  163. );
  164. // Check balances
  165. await expect(ethers.provider.getBalance(this.target)).to.eventually.equal(value1);
  166. await expect(ethers.provider.getBalance(this.anotherTarget)).to.eventually.equal(0);
  167. });
  168. it('reverts with an invalid exec type', async function () {
  169. const value1 = 0x012;
  170. const value2 = 0x234;
  171. const data = encodeBatch(
  172. [this.target, value1, this.target.interface.encodeFunctionData('mockFunction')],
  173. [this.anotherTarget, value2, this.anotherTarget.interface.encodeFunctionData('mockFunction')],
  174. );
  175. await expect(this.utils.$execBatch(data, '0x03'))
  176. .to.be.revertedWithCustomError(this.utils, 'ERC7579UnsupportedExecType')
  177. .withArgs('0x03');
  178. });
  179. });
  180. describe('execDelegateCall', function () {
  181. it('delegate calls the target', async function () {
  182. const slot = ethers.hexlify(ethers.randomBytes(32));
  183. const value = ethers.hexlify(ethers.randomBytes(32));
  184. const data = encodeDelegate(
  185. this.target,
  186. this.target.interface.encodeFunctionData('mockFunctionWritesStorage', [slot, value]),
  187. );
  188. await expect(ethers.provider.getStorage(this.utils.target, slot)).to.eventually.equal(ethers.ZeroHash);
  189. await this.utils.$execDelegateCall(data, EXEC_TYPE_DEFAULT);
  190. await expect(ethers.provider.getStorage(this.utils.target, slot)).to.eventually.equal(value);
  191. });
  192. it('default to calling self is target is address(0) (ERC-7821 calldata compression)', async function () {
  193. const data = encodeDelegate(
  194. ethers.ZeroAddress,
  195. this.utils.interface.encodeFunctionData('$CALLTYPE_DELEGATECALL', []),
  196. );
  197. await expect(this.utils.$execDelegateCall(data, EXEC_TYPE_DEFAULT))
  198. .to.emit(this.utils, 'return$execDelegateCall')
  199. .withArgs([ethers.zeroPadBytes(CALL_TYPE_DELEGATE, 32)]);
  200. });
  201. it('reverts when target reverts in default ExecType', async function () {
  202. const data = encodeDelegate(this.target, this.target.interface.encodeFunctionData('mockFunctionRevertsReason'));
  203. await expect(this.utils.$execDelegateCall(data, EXEC_TYPE_DEFAULT)).to.be.revertedWith(
  204. 'CallReceiverMock: reverting',
  205. );
  206. });
  207. it('emits ERC7579TryExecuteFail event when target reverts in try ExecType', async function () {
  208. const data = encodeDelegate(this.target, this.target.interface.encodeFunctionData('mockFunctionRevertsReason'));
  209. await expect(this.utils.$execDelegateCall(data, EXEC_TYPE_TRY))
  210. .to.emit(this.utils, 'ERC7579TryExecuteFail')
  211. .withArgs(
  212. CALL_TYPE_CALL,
  213. ethers.solidityPacked(
  214. ['bytes4', 'bytes'],
  215. [selector('Error(string)'), coder.encode(['string'], ['CallReceiverMock: reverting'])],
  216. ),
  217. );
  218. });
  219. it('reverts with an invalid exec type', async function () {
  220. const data = encodeDelegate(this.target, this.target.interface.encodeFunctionData('mockFunction'));
  221. await expect(this.utils.$execDelegateCall(data, '0x03'))
  222. .to.be.revertedWithCustomError(this.utils, 'ERC7579UnsupportedExecType')
  223. .withArgs('0x03');
  224. });
  225. });
  226. it('encodes Mode', async function () {
  227. const callType = CALL_TYPE_BATCH;
  228. const execType = EXEC_TYPE_TRY;
  229. const selector = '0x12345678';
  230. const payload = ethers.toBeHex(0, 22);
  231. await expect(this.utils.$encodeMode(callType, execType, selector, payload)).to.eventually.equal(
  232. encodeMode({
  233. callType,
  234. execType,
  235. selector,
  236. payload,
  237. }),
  238. );
  239. });
  240. it('decodes Mode', async function () {
  241. const callType = CALL_TYPE_BATCH;
  242. const execType = EXEC_TYPE_TRY;
  243. const selector = '0x12345678';
  244. const payload = ethers.toBeHex(0, 22);
  245. await expect(
  246. this.utils.$decodeMode(
  247. encodeMode({
  248. callType,
  249. execType,
  250. selector,
  251. payload,
  252. }),
  253. ),
  254. ).to.eventually.deep.equal([callType, execType, selector, payload]);
  255. });
  256. it('encodes single', async function () {
  257. const target = this.target;
  258. const value = 0x123;
  259. const data = '0x12345678';
  260. await expect(this.utils.$encodeSingle(target, value, data)).to.eventually.equal(encodeSingle(target, value, data));
  261. });
  262. it('decodes single', async function () {
  263. const target = this.target;
  264. const value = 0x123;
  265. const data = '0x12345678';
  266. await expect(this.utils.$decodeSingle(encodeSingle(target, value, data))).to.eventually.deep.equal([
  267. target.target,
  268. value,
  269. data,
  270. ]);
  271. });
  272. it('encodes batch', async function () {
  273. const entries = [
  274. [this.target, 0x123, '0x12345678'],
  275. [this.anotherTarget, 0x456, '0x12345678'],
  276. ];
  277. await expect(this.utils.$encodeBatch(entries)).to.eventually.equal(encodeBatch(...entries));
  278. });
  279. it('decodes batch', async function () {
  280. const entries = [
  281. [this.target.target, 0x123, '0x12345678'],
  282. [this.anotherTarget.target, 0x456, '0x12345678'],
  283. ];
  284. await expect(this.utils.$decodeBatch(encodeBatch(...entries))).to.eventually.deep.equal(entries);
  285. });
  286. it('encodes delegate', async function () {
  287. const target = this.target;
  288. const data = '0x12345678';
  289. await expect(this.utils.$encodeDelegate(target, data)).to.eventually.equal(encodeDelegate(target, data));
  290. });
  291. it('decodes delegate', async function () {
  292. const target = this.target;
  293. const data = '0x12345678';
  294. await expect(this.utils.$decodeDelegate(encodeDelegate(target, data))).to.eventually.deep.equal([
  295. target.target,
  296. data,
  297. ]);
  298. });
  299. describe('global', function () {
  300. describe('eqCallTypeGlobal', function () {
  301. it('returns true if both call types are equal', async function () {
  302. await expect(this.utilsGlobal.$eqCallTypeGlobal(CALL_TYPE_BATCH, CALL_TYPE_BATCH)).to.eventually.be.true;
  303. });
  304. it('returns false if both call types are different', async function () {
  305. await expect(this.utilsGlobal.$eqCallTypeGlobal(CALL_TYPE_CALL, CALL_TYPE_BATCH)).to.eventually.be.false;
  306. });
  307. });
  308. describe('eqExecTypeGlobal', function () {
  309. it('returns true if both exec types are equal', async function () {
  310. await expect(this.utilsGlobal.$eqExecTypeGlobal(EXEC_TYPE_TRY, EXEC_TYPE_TRY)).to.eventually.be.true;
  311. });
  312. it('returns false if both exec types are different', async function () {
  313. await expect(this.utilsGlobal.$eqExecTypeGlobal(EXEC_TYPE_DEFAULT, EXEC_TYPE_TRY)).to.eventually.be.false;
  314. });
  315. });
  316. describe('eqModeSelectorGlobal', function () {
  317. it('returns true if both selectors are equal', async function () {
  318. await expect(this.utilsGlobal.$eqModeSelectorGlobal('0x12345678', '0x12345678')).to.eventually.be.true;
  319. });
  320. it('returns false if both selectors are different', async function () {
  321. await expect(this.utilsGlobal.$eqModeSelectorGlobal('0x12345678', '0x87654321')).to.eventually.be.false;
  322. });
  323. });
  324. describe('eqModePayloadGlobal', function () {
  325. it('returns true if both payloads are equal', async function () {
  326. await expect(this.utilsGlobal.$eqModePayloadGlobal(ethers.toBeHex(0, 22), ethers.toBeHex(0, 22))).to.eventually
  327. .be.true;
  328. });
  329. it('returns false if both payloads are different', async function () {
  330. await expect(this.utilsGlobal.$eqModePayloadGlobal(ethers.toBeHex(0, 22), ethers.toBeHex(1, 22))).to.eventually
  331. .be.false;
  332. });
  333. });
  334. });
  335. });