Let us suppose our SIGNER_ADDRESS=0xf11c22d61ecd7b1adcb6b43542fe8a96b9328dc7
and that we do not have the private key,
but we can produce signatures for this account.
https://docs.safe.global/sdk/relay-kit/guides/4337-safe-sdk#initialize-the-safe4337pack
- Requires a Private Key: I can produce signatures, but not a private key
- Having to choose between New vs Existing Safe Account. The init code could determine this fact based on the setup data provided
- calculateSafeOpHash could be part of the kit
- Can not add signature
More details below:
{
owners: string[],
threshold: number,
safeSaltNonce: string
}
Instead, I need to load up a few contracts (singleton, moduleSetup, m4337, proxyFactory) and do all of this to determine the safe address and then check if its deployed to determine how to initialize the safePack
Get Deterministic Safe Address
const setup = await singleton.interface.encodeFunctionData("setup", [
owners,
1, // threshold
moduleSetup.target,
moduleSetup.interface.encodeFunctionData("enableModules", [
[m4337.target],
]),
m4337.target,
ethers.ZeroAddress,
0,
ethers.ZeroAddress,
]);
async addressForSetup(
setup: ethers.BytesLike,
saltNonce?: string,
): Promise<ethers.AddressLike> {
// 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(
["bytes32", "uint256"],
[ethers.keccak256(setup), 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(
["bytes", "uint256"],
[
await this.proxyFactory.proxyCreationCode(),
await this.singleton.getAddress(),
],
);
return ethers.getCreate2Address(
await this.proxyFactory.getAddress(),
salt,
ethers.keccak256(initCode),
)
So, we us use the Safe API to determine if there are any safes with threshold 1 having SIGNER_ADDRESS
as an owner.
We also need to check the fallback handler is a Safe4337Module
a) Safe API Requires Checksum Addresses (why?)
Cool, so we found one -- We can go with "Existing Safe", but... SIGNER_PRIVATE_KEY
(oh no!)
const safe4337Pack = await Safe4337Pack.init({
provider: RPC_URL,
signer: SIGNER_PRIVATE_KEY,
bundlerUrl: `https://api.pimlico.io/v1/sepolia/rpc?apikey=${PIMLICO_API_KEY}`,
options: {
safeAddress: '0x...'
},
// ...
})
So I managed to load the SafeKit (had to specify the custom SAFE_4337_MODULE) as follows:
export async function existingSafe(
signer: string,
chainId: bigint,
): Promise<string | undefined> {
const apiKit = new SafeApiKit({
chainId,
});
const safes = (await apiKit.getSafesByOwner(signer)).safes;
const safeInfos = await Promise.all(
safes.map((safeAddress) => apiKit.getSafeInfo(safeAddress)),
);
const relevantSafes = safeInfos.filter((info) => isRelevantSafe(info));
console.log("Relevant Safes", relevantSafes)
if (relevantSafes.length > 0) {
if (relevantSafes.length > 1) {
console.warn(
`Found multiple relevant Safes for ${signer} - using the first`,
);
}
return relevantSafes[0].address;
}
}
export async function loadSafeKit(
rpcUrl: string,
bundlerUrl: string,
nearSigner: string,
): Promise<Safe4337Pack> {
const signer = sanitizeAddress(nearSigner);
const provider = new ethers.JsonRpcProvider(rpcUrl);
const safeAddress = await existingSafe(
signer,
(await provider.getNetwork()).chainId,
);
let options = safeAddress
? { safeAddress }
: {
owners: [signer],
threshold: 1,
};
const safe4337Pack = await Safe4337Pack.init({
provider: rpcUrl,
bundlerUrl,
options,
customContracts: {
// Why do I need to specify this?
safe4337ModuleAddress: SAFE_4337_MODULE
}
// ...
});
return safe4337Pack;
}
So sad... I am not sure how there could have been a non-trivial signature in this UserOp. Since I did not specify a signer key
Error: could not coalesce error (
error={
"code": -32521,
"message": "UserOperation reverted during simulation with reason: AA23 reverted (or OOG)"
},
payload={
"id": 4,
"jsonrpc": "2.0",
"method": "eth_estimateUserOperationGas",
"params": [ {
"callData": "0x7bb37428000000000000000000000000234cc5b8b9be39fdf0a2e0bced254ead9f4245fc00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000016900000000000000000000000000000000000000000000000000000000000000",
"callGasLimit": "0x01",
"initCode": "0x",
"maxFeePerGas": "0x0965c6a495",
"maxPriorityFeePerGas": "0x1a6576be",
"nonce": "0x00",
"paymasterAndData": "0x",
"preVerificationGas": "0x01",
"sender": "0xb5afd8E64278898191e5B54e18449252De46dE60",
"signature": "0x000000000000000000000000000000000000000000000000234cc5b8b9be39fdf0a2e0bced254ead9f4245fc0000000000000000000000000000000000000000000000000000000000000000010000000000000000000000007f01d9b227593e033bf8d6fc86e634d27aa85568000000000000000000000000000000000000000000000000000000000000000001",
"verificationGasLimit": "0x01"
}, "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789" ] }, code=UNKNOWN_ERROR, version=6.13.1)
So then I proceed in changing the entry point address:
Error: The selected entrypoint 0x0000000071727de22e5e9d8baf0edac6f37da032 is not compatible with version 0.2.0 of Safe modules
So I manage to make the safeOp, but then have to import something else and pass it a bunch of stuff from the kit:
const safeOp = await safeKit.createTransaction({ transactions });
const safeOpHash = calculateSafeUserOperationHash(
safeOp.data,
// This is all part of the kit! could have safeKit.safeOpHash(safeOp)
BigInt(await safeKit.getChainId()),
await safeKit.protocolKit.getFallbackHandler(),
);
So I have produced a signature for safeOp by signing the hash. Now my options are
safeOp.addSignature({signer, data: signature, isContractSignature: false})
for which I am expected to supply:
export interface SafeSignature {
readonly signer: string;
readonly data: string;
readonly isContractSignature: boolean;
// WTF are these?
staticPart(dynamicOffset?: string): string;
dynamicPart(): string;
}
Alternatively there is safeKit.signSafeOperation
which also requires a signer to be present.
No way to manually provide a signature!