Skip to content

Commit

Permalink
replace SafePack with viem
Browse files Browse the repository at this point in the history
  • Loading branch information
bh2smith committed Sep 17, 2024
1 parent e39d0c3 commit c498dc0
Show file tree
Hide file tree
Showing 8 changed files with 444 additions and 137 deletions.
7 changes: 6 additions & 1 deletion examples/send-tx.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import dotenv from "dotenv";
import { ethers } from "ethers";
import { isAddress } from "viem";

import { loadArgs, loadEnv } from "./cli";
import { TransactionManager } from "../src";
Expand Down Expand Up @@ -29,7 +30,11 @@ async function main(): Promise<void> {
},
];
// Add Recovery if safe not deployed & recoveryAddress was provided.
if (!(await txManager.safeDeployed(chainId)) && recoveryAddress) {
if (
!(await txManager.safeDeployed(chainId)) &&
recoveryAddress &&
isAddress(recoveryAddress)
) {
const recoveryTx = txManager.addOwnerTx(recoveryAddress);
// This would happen (sequentially) after the userTx, but all executed in a single
transactions.push(recoveryTx);
Expand Down
4 changes: 3 additions & 1 deletion src/lib/bundler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ export class Erc4337Bundler {
this.entryPointAddress = entryPointAddress;
this.apiKey = apiKey;
this.chainId = chainId;
this.provider = new ethers.JsonRpcProvider(bundlerUrl(chainId, this.apiKey));
this.provider = new ethers.JsonRpcProvider(
bundlerUrl(chainId, this.apiKey)
);
}

async getPaymasterData(
Expand Down
275 changes: 158 additions & 117 deletions src/lib/safe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,21 @@ import {
getSafe4337ModuleDeployment,
getSafeModuleSetupDeployment,
} from "@safe-global/safe-modules-deployments";
import { ethers } from "ethers";
import { Address, Hash, Hex } from "viem";
import { Network } from "near-ca";
import {
Address,
encodeFunctionData,
encodePacked,
getCreate2Address,
Hash,
Hex,
keccak256,
ParseAbi,
parseAbi,
PublicClient,
toHex,
zeroAddress,
} from "viem";

import {
GasPrice,
Expand All @@ -17,26 +30,31 @@ import {
} from "../types";
import { PLACEHOLDER_SIG, packGas, packPaymasterData } from "../util";

interface DeploymentData {
abi: unknown[] | ParseAbi<readonly string[]>;
address: `0x${string}`;
}

/**
* All contracts used in account creation & execution
*/
export class ContractSuite {
provider: ethers.JsonRpcProvider;
singleton: ethers.Contract;
proxyFactory: ethers.Contract;
m4337: ethers.Contract;
moduleSetup: ethers.Contract;
entryPoint: ethers.Contract;
client: PublicClient;
singleton: DeploymentData;
proxyFactory: DeploymentData;
m4337: DeploymentData;
moduleSetup: DeploymentData;
entryPoint: DeploymentData;

constructor(
provider: ethers.JsonRpcProvider,
singleton: ethers.Contract,
proxyFactory: ethers.Contract,
m4337: ethers.Contract,
moduleSetup: ethers.Contract,
entryPoint: ethers.Contract
client: PublicClient,
singleton: DeploymentData,
proxyFactory: DeploymentData,
m4337: DeploymentData,
moduleSetup: DeploymentData,
entryPoint: DeploymentData
) {
this.provider = provider;
this.client = client;
this.singleton = singleton;
this.proxyFactory = proxyFactory;
this.m4337 = m4337;
Expand All @@ -46,89 +64,106 @@ export class ContractSuite {

static async init(): Promise<ContractSuite> {
// TODO - this is a cheeky hack.
const provider = new ethers.JsonRpcProvider("https://rpc2.sepolia.org");
const safeDeployment = (fn: DeploymentFunction): Promise<ethers.Contract> =>
getDeployment(fn, { provider, version: "1.4.1" });
const client = Network.fromChainId(11155111).client;
const safeDeployment = (fn: DeploymentFunction): Promise<DeploymentData> =>
getDeployment(fn, { version: "1.4.1" });
const m4337Deployment = async (
fn: DeploymentFunction
): Promise<ethers.Contract> => {
return getDeployment(fn, { provider, version: "0.3.0" });
): Promise<DeploymentData> => {
return getDeployment(fn, { version: "0.3.0" });
};
// Need this first to get entryPoint address
const m4337 = await m4337Deployment(getSafe4337ModuleDeployment);

const [singleton, proxyFactory, moduleSetup, supportedEntryPoint] =
await Promise.all([
safeDeployment(getSafeL2SingletonDeployment),
safeDeployment(getProxyFactoryDeployment),
m4337Deployment(getSafeModuleSetupDeployment),
m4337.SUPPORTED_ENTRYPOINT(),
]);
const entryPoint = new ethers.Contract(
supportedEntryPoint,
["function getNonce(address, uint192 key) view returns (uint256 nonce)"],
provider
);
console.log("Initialized ERC4337 & Safe Module Contracts:", {
singleton: await singleton.getAddress(),
proxyFactory: await proxyFactory.getAddress(),
m4337: await m4337.getAddress(),
moduleSetup: await moduleSetup.getAddress(),
entryPoint: await entryPoint.getAddress(),
});

const [singleton, proxyFactory, moduleSetup, m4337] = await Promise.all([
safeDeployment(getSafeL2SingletonDeployment),
safeDeployment(getProxyFactoryDeployment),
m4337Deployment(getSafeModuleSetupDeployment),
m4337Deployment(getSafe4337ModuleDeployment),
]);

// console.log("Initialized ERC4337 & Safe Module Contracts:", {
// singleton: await singleton.getAddress(),
// proxyFactory: await proxyFactory.getAddress(),
// m4337: await m4337.getAddress(),
// moduleSetup: await moduleSetup.getAddress(),
// entryPoint: await entryPoint.getAddress(),
// });
return new ContractSuite(
provider,
client,
singleton,
proxyFactory,
m4337,
moduleSetup,
entryPoint
// EntryPoint:
{
address: (await client.readContract({
address: m4337.address,
abi: m4337.abi,
functionName: "SUPPORTED_ENTRYPOINT",
})) as Address,
abi: parseAbi([
"function getNonce(address, uint192 key) view returns (uint256 nonce)",
]),
}
);
}

async addressForSetup(
setup: ethers.BytesLike,
saltNonce?: string
): Promise<Address> {
async addressForSetup(setup: Hex, saltNonce?: string): Promise<Address> {
// bytes32 salt = keccak256(abi.encodePacked(keccak256(initializer), saltNonce));
// cf: https://github.com/safe-global/safe-smart-account/blob/499b17ad0191b575fcadc5cb5b8e3faeae5391ae/contracts/proxies/SafeProxyFactory.sol#L58
const salt = ethers.keccak256(
ethers.solidityPacked(
const salt = keccak256(encodePacked(
["bytes32", "uint256"],
[ethers.keccak256(setup), saltNonce || 0]
[keccak256(setup), BigInt(saltNonce || "0")]
)
);

// abi.encodePacked(type(SafeProxy).creationCode, uint256(uint160(_singleton)));
// cf: https://github.com/safe-global/safe-smart-account/blob/499b17ad0191b575fcadc5cb5b8e3faeae5391ae/contracts/proxies/SafeProxyFactory.sol#L29
const initCode = ethers.solidityPacked(
const initCode = encodePacked(
["bytes", "uint256"],
[
await this.proxyFactory.proxyCreationCode(),
await this.singleton.getAddress(),
(await this.client.readContract({
address: this.proxyFactory.address,
abi: this.proxyFactory.abi,
functionName: "proxyCreationCode",
})) as Hex,
BigInt(this.singleton.address),
]
);
return ethers.getCreate2Address(
await this.proxyFactory.getAddress(),
return getCreate2Address({
from: this.proxyFactory.address,
salt,
ethers.keccak256(initCode)
) as Address;
bytecodeHash: keccak256(initCode),
});
}

async getSetup(owners: string[]): Promise<Hex> {
const setup = await this.singleton.interface.encodeFunctionData("setup", [
owners,
1, // We use sign threshold of 1.
this.moduleSetup.target,
this.moduleSetup.interface.encodeFunctionData("enableModules", [
[this.m4337.target],
]),
this.m4337.target,
ethers.ZeroAddress,
0,
ethers.ZeroAddress,
]);
return setup as Hex;

getSetup(owners: string[]): Hex {
return encodeFunctionData({
abi: this.singleton.abi,
functionName: "setup",
args: [
owners,
1, // We use sign threshold of 1.
this.moduleSetup.address,
encodeFunctionData({
abi: this.moduleSetup.abi,
functionName: "enableModules",
args: [[this.m4337.address]],
}),
this.m4337.address,
zeroAddress,
0,
zeroAddress,
],
});
}

addOwnerData(newOwner: Address): Hex {
return encodeFunctionData({
abi: this.singleton.abi,
functionName: "addOwnerWithThreshold",
args: [newOwner, 1],
});
}

async getOpHash(unsignedUserOp: UserOperation): Promise<Hash> {
Expand All @@ -140,30 +175,39 @@ export class ContractSuite {
maxPriorityFeePerGas,
maxFeePerGas,
} = unsignedUserOp;
return this.m4337.getOperationHash({
...unsignedUserOp,
initCode: factory
? ethers.solidityPacked(["address", "bytes"], [factory, factoryData])
: "0x",
accountGasLimits: packGas(verificationGasLimit, callGasLimit),
gasFees: packGas(maxPriorityFeePerGas, maxFeePerGas),
paymasterAndData: packPaymasterData(unsignedUserOp),
signature: PLACEHOLDER_SIG,
const opHash = await this.client.readContract({
address: this.m4337.address,
abi: this.m4337.abi,
functionName: "getOperationHash",
args: [
{
...unsignedUserOp,
initCode: factory
? encodePacked(["address", "bytes"], [factory, factoryData!])
: "0x",
accountGasLimits: packGas(verificationGasLimit, callGasLimit),
gasFees: packGas(maxPriorityFeePerGas, maxFeePerGas),
paymasterAndData: packPaymasterData(unsignedUserOp),
signature: PLACEHOLDER_SIG,
},
],
});
return opHash as Hash;
}

factoryDataForSetup(
private factoryDataForSetup(
safeNotDeployed: boolean,
setup: string,
safeSaltNonce: string
): { factory?: Address; factoryData?: Hex } {
return safeNotDeployed
? {
factory: this.proxyFactory.target as Address,
factoryData: this.proxyFactory.interface.encodeFunctionData(
"createProxyWithNonce",
[this.singleton.target, setup, safeSaltNonce]
) as Hex,
factory: this.proxyFactory.address,
factoryData: encodeFunctionData({
abi: this.proxyFactory.abi,
functionName: "createProxyWithNonce",
args: [this.singleton.address, setup, safeSaltNonce],
}),
}
: {};
}
Expand All @@ -176,20 +220,29 @@ export class ContractSuite {
safeNotDeployed: boolean,
safeSaltNonce: string
): Promise<UnsignedUserOperation> {
const rawUserOp = {
const nonce = (await this.client.readContract({
abi: this.entryPoint.abi,
address: this.entryPoint.address,
functionName: "getNonce",
args: [safeAddress, 0],
})) as bigint;
return {
sender: safeAddress,
nonce: ethers.toBeHex(await this.entryPoint.getNonce(safeAddress, 0)),
nonce: toHex(nonce),
...this.factoryDataForSetup(safeNotDeployed, setup, safeSaltNonce),
// <https://github.com/safe-global/safe-modules/blob/9a18245f546bf2a8ed9bdc2b04aae44f949ec7a0/modules/4337/contracts/Safe4337Module.sol#L172>
callData: this.m4337.interface.encodeFunctionData("executeUserOp", [
txData.to,
BigInt(txData.value),
txData.data,
txData.operation || 0,
]) as Hex,
callData: encodeFunctionData({
abi: this.m4337.abi,
functionName: "executeUserOp",
args: [
txData.to,
BigInt(txData.value),
txData.data,
txData.operation || 0,
],
}),
...feeData,
};
return rawUserOp;
}
}

Expand All @@ -198,31 +251,19 @@ type DeploymentFunction = (filter?: {
}) =>
| { networkAddresses: { [chainId: string]: string }; abi: unknown[] }
| undefined;
type DeploymentArgs = { provider: ethers.JsonRpcProvider; version: string };
type DeploymentArgs = { version: string };

async function getDeployment(
fn: DeploymentFunction,
{ provider, version }: DeploymentArgs
): Promise<ethers.Contract> {
const { chainId } = await provider.getNetwork();
{ version }: DeploymentArgs
): Promise<DeploymentData> {
const deployment = fn({ version });
if (!deployment) {
throw new Error(
`Deployment not found for ${fn.name} version ${version} on chainId ${chainId}`
);
}
let address = deployment.networkAddresses[`${chainId}`];
if (!address) {
// console.warn(
// `Deployment asset ${fn.name} not listed on chainId ${chainId}, using likely fallback. For more info visit https://github.com/safe-global/safe-modules-deployments`
// );
// TODO: This is a cheeky hack. Real solution proposed in
// https://github.com/Mintbase/near-safe/issues/42
address = deployment.networkAddresses["11155111"];
throw new Error(`Deployment not found for ${fn.name} version ${version}`);
}
return new ethers.Contract(
address,
deployment.abi as ethers.Fragment[],
provider
);
// TODO: maybe call parseAbi on deployment.abi here.
return {
address: deployment.networkAddresses["11155111"] as Address,
abi: deployment.abi,
};
}
Loading

0 comments on commit c498dc0

Please sign in to comment.