@owlprotocol/contracts-account-abstraction
Version:
ERC4337 Account Abstraction support for deploying relevant smart contracts and interacting with UserOps.
311 lines (287 loc) • 11 kB
text/typescript
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 };
}