erc4337.js 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. const { ethers, config, predeploy } = require('hardhat');
  2. const SIG_VALIDATION_SUCCESS = '0x0000000000000000000000000000000000000000';
  3. const SIG_VALIDATION_FAILURE = '0x0000000000000000000000000000000000000001';
  4. function getAddress(account) {
  5. return account.target ?? account.address ?? account;
  6. }
  7. function pack(left, right) {
  8. return ethers.solidityPacked(['uint128', 'uint128'], [left, right]);
  9. }
  10. function packValidationData(validAfter, validUntil, authorizer) {
  11. return ethers.solidityPacked(
  12. ['uint48', 'uint48', 'address'],
  13. [
  14. validAfter,
  15. validUntil,
  16. typeof authorizer == 'boolean'
  17. ? authorizer
  18. ? SIG_VALIDATION_SUCCESS
  19. : SIG_VALIDATION_FAILURE
  20. : getAddress(authorizer),
  21. ],
  22. );
  23. }
  24. function packInitCode(factory, factoryData) {
  25. return ethers.solidityPacked(['address', 'bytes'], [getAddress(factory), factoryData]);
  26. }
  27. function packPaymasterAndData(paymaster, paymasterVerificationGasLimit, paymasterPostOpGasLimit, paymasterData) {
  28. return ethers.solidityPacked(
  29. ['address', 'uint128', 'uint128', 'bytes'],
  30. [getAddress(paymaster), paymasterVerificationGasLimit, paymasterPostOpGasLimit, paymasterData],
  31. );
  32. }
  33. /// Represent one user operation
  34. class UserOperation {
  35. constructor(params) {
  36. this.sender = getAddress(params.sender);
  37. this.nonce = params.nonce;
  38. this.factory = params.factory ?? undefined;
  39. this.factoryData = params.factoryData ?? '0x';
  40. this.callData = params.callData ?? '0x';
  41. this.verificationGas = params.verificationGas ?? 10_000_000n;
  42. this.callGas = params.callGas ?? 100_000n;
  43. this.preVerificationGas = params.preVerificationGas ?? 100_000n;
  44. this.maxPriorityFee = params.maxPriorityFee ?? 100_000n;
  45. this.maxFeePerGas = params.maxFeePerGas ?? 100_000n;
  46. this.paymaster = params.paymaster ?? undefined;
  47. this.paymasterVerificationGasLimit = params.paymasterVerificationGasLimit ?? 0n;
  48. this.paymasterPostOpGasLimit = params.paymasterPostOpGasLimit ?? 0n;
  49. this.paymasterData = params.paymasterData ?? '0x';
  50. this.signature = params.signature ?? '0x';
  51. }
  52. get packed() {
  53. return {
  54. sender: this.sender,
  55. nonce: this.nonce,
  56. initCode: this.factory ? packInitCode(this.factory, this.factoryData) : '0x',
  57. callData: this.callData,
  58. accountGasLimits: pack(this.verificationGas, this.callGas),
  59. preVerificationGas: this.preVerificationGas,
  60. gasFees: pack(this.maxPriorityFee, this.maxFeePerGas),
  61. paymasterAndData: this.paymaster
  62. ? packPaymasterAndData(
  63. this.paymaster,
  64. this.paymasterVerificationGasLimit,
  65. this.paymasterPostOpGasLimit,
  66. this.paymasterData,
  67. )
  68. : '0x',
  69. signature: this.signature,
  70. };
  71. }
  72. hash(entrypoint) {
  73. return entrypoint.getUserOpHash(this.packed);
  74. }
  75. }
  76. const parseInitCode = initCode => ({
  77. factory: '0x' + initCode.replace(/0x/, '').slice(0, 40),
  78. factoryData: '0x' + initCode.replace(/0x/, '').slice(40),
  79. });
  80. /// Global ERC-4337 environment helper.
  81. class ERC4337Helper {
  82. constructor() {
  83. this.factoryAsPromise = ethers.deployContract('$Create2');
  84. }
  85. async wait() {
  86. this.factory = await this.factoryAsPromise;
  87. return this;
  88. }
  89. async newAccount(name, extraArgs = [], params = {}) {
  90. const env = {
  91. entrypoint: params.entrypoint ?? predeploy.entrypoint.v08,
  92. senderCreator: params.senderCreator ?? predeploy.senderCreator.v08,
  93. };
  94. const { factory } = await this.wait();
  95. const accountFactory = await ethers.getContractFactory(name);
  96. if (params.erc7702signer) {
  97. const delegate = await accountFactory.deploy(...extraArgs);
  98. const instance = await params.erc7702signer.getAddress().then(address => accountFactory.attach(address));
  99. const authorization = await params.erc7702signer.authorize({ address: delegate.target });
  100. return new ERC7702SmartAccount(instance, authorization, env);
  101. } else {
  102. const initCode = await accountFactory
  103. .getDeployTransaction(...extraArgs)
  104. .then(tx =>
  105. factory.interface.encodeFunctionData('$deploy', [0, params.salt ?? ethers.randomBytes(32), tx.data]),
  106. )
  107. .then(deployCode => ethers.concat([factory.target, deployCode]));
  108. const instance = await ethers.provider
  109. .call({
  110. from: env.entrypoint,
  111. to: env.senderCreator,
  112. data: env.senderCreator.interface.encodeFunctionData('createSender', [initCode]),
  113. })
  114. .then(result => ethers.getAddress(ethers.hexlify(ethers.getBytes(result).slice(-20))))
  115. .then(address => accountFactory.attach(address));
  116. return new SmartAccount(instance, initCode, env);
  117. }
  118. }
  119. }
  120. /// Represent one ERC-4337 account contract.
  121. class SmartAccount extends ethers.BaseContract {
  122. constructor(instance, initCode, env) {
  123. super(instance.target, instance.interface, instance.runner, instance.deployTx);
  124. this.address = instance.target;
  125. this.initCode = initCode;
  126. this._env = env;
  127. }
  128. async deploy(account = this.runner) {
  129. const { factory: to, factoryData: data } = parseInitCode(this.initCode);
  130. this.deployTx = await account.sendTransaction({ to, data });
  131. return this;
  132. }
  133. async createUserOp(userOp = {}) {
  134. userOp.sender ??= this;
  135. userOp.nonce ??= await this._env.entrypoint.getNonce(userOp.sender, 0);
  136. if (ethers.isAddressable(userOp.paymaster)) {
  137. userOp.paymaster = await ethers.resolveAddress(userOp.paymaster);
  138. userOp.paymasterVerificationGasLimit ??= 100_000n;
  139. userOp.paymasterPostOpGasLimit ??= 100_000n;
  140. }
  141. return new UserOperationWithContext(userOp, this._env);
  142. }
  143. }
  144. class ERC7702SmartAccount extends SmartAccount {
  145. constructor(instance, authorization, env) {
  146. super(instance, undefined, env);
  147. this.authorization = authorization;
  148. }
  149. async deploy() {
  150. // hardhat signers from @nomicfoundation/hardhat-ethers do not support type 4 txs.
  151. // so we rebuild it using "native" ethers
  152. await ethers.Wallet.fromPhrase(config.networks.hardhat.accounts.mnemonic, ethers.provider).sendTransaction({
  153. to: ethers.ZeroAddress,
  154. authorizationList: [this.authorization],
  155. gasLimit: 46_000n, // 21,000 base + PER_EMPTY_ACCOUNT_COST
  156. });
  157. return this;
  158. }
  159. }
  160. class UserOperationWithContext extends UserOperation {
  161. constructor(userOp, env) {
  162. super(userOp);
  163. this._sender = userOp.sender;
  164. this._env = env;
  165. }
  166. addInitCode() {
  167. if (this._sender?.initCode) {
  168. return Object.assign(this, parseInitCode(this._sender.initCode));
  169. } else throw new Error('No init code available for the sender of this user operation');
  170. }
  171. getAuthorization() {
  172. if (this._sender?.authorization) {
  173. return this._sender.authorization;
  174. } else throw new Error('No EIP-7702 authorization available for the sender of this user operation');
  175. }
  176. hash() {
  177. return super.hash(this._env.entrypoint);
  178. }
  179. }
  180. module.exports = {
  181. SIG_VALIDATION_SUCCESS,
  182. SIG_VALIDATION_FAILURE,
  183. packValidationData,
  184. packInitCode,
  185. packPaymasterAndData,
  186. UserOperation,
  187. ERC4337Helper,
  188. };