UNPKG

@owlprotocol/contracts-account-abstraction

Version:

ERC4337 Account Abstraction support for deploying relevant smart contracts and interacting with UserOps.

311 lines (287 loc) 11 kB
import { getOrDeployDeterministicContract, DETERMINISTIC_DEPLOYER_ADDRESS, getDeployDeterministicAddress, getOrPrepareDeterministicContract, } from "@owlprotocol/viem-utils"; import { Address, Hash, encodeDeployData, formatEther, zeroHash, Account, Chain, Client, Transport, TransactionRequest, } from "viem"; import { entryPoint07Address } from "viem/account-abstraction"; import { getCode, readContract, sendTransaction, simulateContract, waitForTransactionReceipt, writeContract, } from "viem/actions"; import { getAction } from "viem/utils"; import { ENTRYPOINT_SALT_V07 } from "./constants.js"; import { EntryPoint } from "./artifacts/EntryPoint.js"; import { SimpleAccountFactory } from "./artifacts/SimpleAccountFactory.js"; import { VerifyingPaymaster } from "./artifacts/VerifyingPaymaster.js"; import { EntryPointSimulations } from "./artifacts/EntryPointSimulations.js"; import { PimlicoEntryPointSimulations } from "./artifacts/PimlicoEntryPointSimulations.js"; /** * Get public ERC4337 contracts. * @returns */ export function getERC4337Contracts() { const deterministicDeployer = DETERMINISTIC_DEPLOYER_ADDRESS; const entrypoint = getDeployDeterministicAddress({ salt: ENTRYPOINT_SALT_V07, bytecode: EntryPoint.bytecode }); const simpleAccountFactory = getDeployDeterministicAddress({ salt: zeroHash, bytecode: encodeDeployData({ abi: SimpleAccountFactory.abi, bytecode: SimpleAccountFactory.bytecode, args: [entrypoint], }), }); const entrypointSimulations = getDeployDeterministicAddress({ salt: zeroHash, bytecode: encodeDeployData({ abi: EntryPointSimulations.abi, bytecode: EntryPointSimulations.bytecode, args: [], }), }); const pimlicoEntrypointSimulations = getDeployDeterministicAddress({ salt: zeroHash, bytecode: encodeDeployData({ abi: PimlicoEntryPointSimulations.abi, bytecode: PimlicoEntryPointSimulations.bytecode, args: [entrypointSimulations], }), }); return { deterministicDeployer, entrypoint: entrypoint as typeof entryPoint07Address, simpleAccountFactory, entrypointSimulations, pimlicoEntrypointSimulations, }; } export const erc4337Contracts = getERC4337Contracts(); /** * Prepare erc4337 deployment transactions. Useful to do gas estimations before sending transaction * @param client with account */ export async function prepareERC4337Contracts(client: Client<Transport, Chain, Account>) { const requests: TransactionRequest[] = []; const [entrypoint, simpleAccountFactory, entrypointSimulations, pimlicoEntrypointSimulations] = await Promise.all([ getOrPrepareDeterministicContract( client, //Extracted salt (first 32 bytes) from original tx //https://etherscan.io/tx/0x5c81ea86f6c54481d3e21c78675b4f1d985c1fa62b678dcdfdf7934ddd6e127e { salt: ENTRYPOINT_SALT_V07, bytecode: EntryPoint.bytecode, }, ), getOrPrepareDeterministicContract(client, { salt: zeroHash, bytecode: encodeDeployData({ abi: SimpleAccountFactory.abi, bytecode: SimpleAccountFactory.bytecode, args: [erc4337Contracts.entrypoint], }), }), getOrPrepareDeterministicContract(client, { salt: zeroHash, bytecode: encodeDeployData({ abi: EntryPointSimulations.abi, bytecode: EntryPointSimulations.bytecode, args: [], }), }), getOrPrepareDeterministicContract(client, { salt: zeroHash, bytecode: encodeDeployData({ abi: PimlicoEntryPointSimulations.abi, bytecode: PimlicoEntryPointSimulations.bytecode, args: [erc4337Contracts.entrypointSimulations], }), }), ]); if (entrypoint.address != entryPoint07Address) { throw new Error( `Entrypoint v0.7 deployed address ${entryPoint07Address} (expected) != ${entrypoint.address} (actual)`, ); } if (entrypoint.request) requests.push(entrypoint.request); if (simpleAccountFactory.request) requests.push(simpleAccountFactory.request); if (entrypointSimulations.request) requests.push(entrypointSimulations.request); if (pimlicoEntrypointSimulations.request) requests.push(pimlicoEntrypointSimulations.request); return { requests, entrypoint: entrypoint as { address: typeof entryPoint07Address; request: TransactionRequest | undefined; existed: boolean; }, simpleAccountFactory, entrypointSimulations, pimlicoEntrypointSimulations, }; } /** * Deploy public ERC4337 contracts. * These have no permissions system whatsoever and are shared contract infrastructure. * These are often pre-deployed on certain frameworks (eg. OPStack) and can be used by anyone. * This guarantees compatibility on any EVM chain. * - DeterministicDeployer (0x4e59b44847b379578588920cA78FbF26c0B4956C) * - EntryPointV07 (0x0000000071727De22E5E9d8BAf0edAc6f37da032) * - SimpleAccountFactory (0x91E60e0613810449d098b0b5Ec8b51A0FE8c8985) * @param client Client with account and nonceManager * @returns contract info */ export async function setupERC4337Contracts(client: Client<Transport, Chain, Account>) { if (!client.account.nonceManager) { throw new Error("client.account.nonceManager undefined"); } const erc4337Contracts = await prepareERC4337Contracts(client); const transactions = await Promise.all( erc4337Contracts.requests.map((request) => getAction(client, sendTransaction, "sendTransaction")(request as any), ), ); const receipts = await Promise.all( transactions.map((hash) => getAction(client, waitForTransactionReceipt, "waitForTransactionReceipt")({ hash })), ); return { ...erc4337Contracts, transactions, receipts, }; } /** * Get VerifyingPaymaster contract. * @param param0 * @return TODO: <address> (production) | 0x2e23ef1375aA642504bED97676A566F5A3E4ae5A (staging) */ export function getVerifyingPaymaster(params: { verifyingSignerAddress: Address }): Address { const { verifyingSignerAddress } = params; return getDeployDeterministicAddress({ salt: zeroHash, bytecode: encodeDeployData({ abi: VerifyingPaymaster.abi, bytecode: VerifyingPaymaster.bytecode, args: [entryPoint07Address, verifyingSignerAddress], }), }); } /** * Deploy ERC4337 VerifyingPaymaster. * This contracts stores a balance of ETH to sponsor UserOps (aka "Paymaster"). * It approves UserOp sponsorship if these are signed by the `verifyingSignerAddress` (the "Verifying" part). * - VerifyingPaymaster () //TODO: TBD after research if we should deploy AA stack. * @param client Client with chain & account for deploying contracts, verifyingSignerAddress address for paymaster * @returns contract info * */ export async function setupVerifyingPaymaster( client: Client<Transport, Chain, Account>, parameters: { verifyingSignerAddress: Address; }, ) { const { verifyingSignerAddress } = parameters; const entrypointBytecode = await getAction(client, getCode, "getCode")({ address: entryPoint07Address }); if (!entrypointBytecode) { throw new Error( `EntryPoint v0.7 ${entryPoint07Address} not deployed. Consider deploying with setupERC4337Contracts()`, ); } //If no VerifyingPaymaster, wait for deploy (mostly used for local testing) //EntryPoint MUST be deployed for this to work const verifyingPaymaster = await getOrDeployDeterministicContract(client, { salt: zeroHash, bytecode: encodeDeployData({ abi: VerifyingPaymaster.abi, bytecode: VerifyingPaymaster.bytecode, args: [entryPoint07Address, verifyingSignerAddress], }), }); // console.debug(verifyingPaymaster); if (verifyingPaymaster.hash) { await getAction( client, waitForTransactionReceipt, "waitForTransactionReceipt", )({ hash: verifyingPaymaster.hash }); } return verifyingPaymaster; } /** * Topup Paymaster contract with funds if balance is below `minBalance` (0 = Always topup) * * Funds are deposited to reach `targetBalance` (defaults to `minBalance`) * @param parameters publicClient, walletClient **with funds**, minBalance, targetBalance * @returns current paymaster balance & transaction hash for topup (if required) */ export async function topupPaymaster( client: Client<Transport, Chain, Account>, parameters: { paymaster: Address; minBalance: bigint; targetBalance?: bigint; }, ): Promise<{ balance: bigint; hash?: Hash }> { const { paymaster, minBalance } = parameters; if (minBalance == 0n && parameters.targetBalance === undefined) { //Ensure invariant targetBalance ALWAYS defined with minBalance = 0 throw new Error(`topupAddressL2: minBalance 0, targetBalance MUST be defined`); } const targetBalance = parameters.targetBalance ?? minBalance; if (minBalance > targetBalance) { //Ensure invariant targetBalance >= minBalance throw new Error( `topupPaymaster: minBalance (${formatEther(minBalance)}) > targetBalance (${formatEther(targetBalance)})`, ); } if (0n >= targetBalance) { //Ensure invariant targetBalance > 0 throw new Error(`topupPaymaster: 0 >= targetBalance (${formatEther(targetBalance)})`); } const balance = await getAction( client, readContract, "readContract", )({ address: entryPoint07Address, abi: EntryPoint.abi, functionName: "balanceOf", args: [paymaster], }); // Amount to topup const targetDeficit = targetBalance - balance; if (targetDeficit > 0n && (balance < minBalance || minBalance == 0n)) { //Paymaster under-funded => deposit from wallet account const paymasterDeposit = await getAction( client, simulateContract, "simulateContract", )({ account: client.account, chain: client.chain, address: paymaster, abi: VerifyingPaymaster.abi, functionName: "deposit", value: targetDeficit, args: [], }); const paymasterDepositHash = await getAction(client, writeContract, "writeContract")(paymasterDeposit.request); return { balance, hash: paymasterDepositHash }; } return { balance }; }