-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathsetupERC4337Contracts.ts
310 lines (287 loc) · 10.8 KB
/
setupERC4337Contracts.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
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 };
}