Skip to content
This repository has been archived by the owner on Sep 30, 2024. It is now read-only.

Demonstrate SafeECDSAPlugin in in-page wallet #97

Merged
merged 10 commits into from
Sep 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions account-integrations/safe/.env.example
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
ERC4337_TEST_BUNDLER_URL=http://localhost:3000/rpc
ERC4337_TEST_NODE_URL=http://localhost:8545
ERC4337_TEST_SINGLETON_ADDRESS=0x1CD01F9a3174866c8C89467cb34804E1D5078AC3
ERC4337_TEST_SAFE_FACTORY_ADDRESS=0xDefeAee1F5188da39dab724a95eE0B347c598577
MNEMONIC="test test test test test test test test test test test junk"
8 changes: 6 additions & 2 deletions account-integrations/safe/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,18 @@ cp .env.example .env

3. Setup and run an external bundler (make sure the values in `.env` match up with the bundler and node you're running).

For example, [eth-infinitism/bundler](https://github.com/eth-infinitism/bundler).

```bash
# If using the eth-infinitism bundler, checkout to this commmit. The latest version of the bundler has started breaking the integration tests. This is a previous commit where the integration tests still pass
git checkout 1b154c9
```

You will probably need to fund the address used by the bundler, eg:

```bash
# If using the eth-infinitism bundler
yarn run bundler
# In this repo
yarn hardhat --network localhost sendEth --address INSERT_BUNDLER_ADDRESS
```

4. Run the plugin tests:
Expand Down
25 changes: 23 additions & 2 deletions account-integrations/safe/hardhat.config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { HardhatUserConfig } from "hardhat/config";
import { HardhatUserConfig, task, types } from "hardhat/config";
import "@nomicfoundation/hardhat-toolbox";
import "hardhat-preprocessor";
import fs from "fs";
Expand All @@ -16,7 +16,7 @@ function getRemappings() {

const config: HardhatUserConfig = {
solidity: {
version: "0.8.12",
version: "0.8.19",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is probably fine, but bumping solidity versions should be a considered step. If we're using a library that was audited for a particular version of compiler, we should keep that version of compiler in the list, and add any later additional versions (like we did in blswallet for blslibs).
(Related: #94)

settings: {
optimizer: {
enabled: true,
Expand Down Expand Up @@ -58,3 +58,24 @@ const config: HardhatUserConfig = {
};

export default config;

task("sendEth", "Sends ETH to an address")
.addParam("address", "Address to send ETH to", undefined, types.string)
.addOptionalParam("amount", "Amount of ETH to send", "1.0")
.setAction(
async ({ address, amount }: { address: string; amount: string }, hre) => {
const wallet = hre.ethers.Wallet.fromPhrase(
"test ".repeat(11) + "junk",
hre.ethers.provider,
);

console.log(`${wallet.address} -> ${address} ${amount} ETH`);

const txnRes = await wallet.sendTransaction({
to: address,
value: hre.ethers.parseEther(amount),
});

await txnRes.wait();
},
);
2 changes: 2 additions & 0 deletions account-integrations/safe/script/deploy_all.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
SignMessageLib__factory,
SafeL2__factory,
Safe__factory,
EntryPoint__factory,
} from "../typechain-types";

async function deploy() {
Expand All @@ -20,6 +21,7 @@ async function deploy() {
TokenCallbackHandler__factory,
CompatibilityFallbackHandler__factory,
CreateCall__factory,
EntryPoint__factory,
MultiSend__factory,
MultiSendCallOnly__factory,
SignMessageLib__factory,
Expand Down
47 changes: 47 additions & 0 deletions account-integrations/safe/src/SafeECDSAFactory.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity >=0.7.0 <0.9.0;
pragma abicoder v2;

import {Safe} from "safe-contracts/contracts/Safe.sol";
import {SafeProxyFactory} from "safe-contracts/contracts/proxies/SafeProxyFactory.sol";
import {SafeProxy} from "safe-contracts/contracts/proxies/SafeProxy.sol";

import {EntryPoint} from "account-abstraction/contracts/core/EntryPoint.sol";

import {SafeECDSAPlugin} from "./SafeECDSAPlugin.sol";

contract SafeECDSAFactory {
function create(
Safe safeSingleton,
EntryPoint entryPoint,
address owner,
uint256 saltNonce
) external returns (SafeECDSAPlugin) {
bytes32 salt = keccak256(abi.encodePacked(owner, saltNonce));

Safe safe = Safe(payable(new SafeProxy{salt: salt}(
address(safeSingleton)
)));

address[] memory owners = new address[](1);
owners[0] = owner;

SafeECDSAPlugin plugin = new SafeECDSAPlugin{salt: salt}(
address(entryPoint),
owner
);

safe.setup(
owners,
1,
address(plugin),
abi.encodeCall(SafeECDSAPlugin.enableMyself, ()),
address(plugin),
address(0),
0,
payable(address(0))
);

return SafeECDSAPlugin(address(safe));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,21 @@ import { UserOperationStruct } from "@account-abstraction/contracts";
import { getUserOpHash } from "@account-abstraction/utils";
import { calculateProxyAddress } from "../utils/calculateProxyAddress";
import {
SafeECDSAFactory__factory,
SafeECDSAPlugin__factory,
SafeProxyFactory__factory,
Safe__factory,
} from "../../../typechain-types";
import sendUserOpAndWait from "../utils/sendUserOpAndWait";
import receiptOf from "../utils/receiptOf";
import SafeSingletonFactory from "../utils/SafeSingletonFactory";

const ERC4337_TEST_ENV_VARIABLES_DEFINED =
typeof process.env.ERC4337_TEST_BUNDLER_URL !== "undefined" &&
typeof process.env.ERC4337_TEST_NODE_URL !== "undefined" &&
typeof process.env.ERC4337_TEST_SAFE_FACTORY_ADDRESS !== "undefined" &&
typeof process.env.ERC4337_TEST_SINGLETON_ADDRESS !== "undefined" &&
typeof process.env.MNEMONIC !== "undefined";

const itif = ERC4337_TEST_ENV_VARIABLES_DEFINED ? it : it.skip;
const SAFE_FACTORY_ADDRESS = process.env.ERC4337_TEST_SAFE_FACTORY_ADDRESS;
const SINGLETON_ADDRESS = process.env.ERC4337_TEST_SINGLETON_ADDRESS;
const BUNDLER_URL = process.env.ERC4337_TEST_BUNDLER_URL;
const NODE_URL = process.env.ERC4337_TEST_NODE_URL;
const MNEMONIC = process.env.MNEMONIC;
Expand All @@ -36,24 +35,16 @@ describe("SafeECDSAPlugin", () => {
"eth_supportedEntryPoints",
[],
)) as string[];

if (entryPoints.length === 0) {
throw new Error("No entry points found");
}

if (!SAFE_FACTORY_ADDRESS) {
throw new Error("No Safe factory address found");
}

if (!SINGLETON_ADDRESS) {
throw new Error("No Safe singleton address found");
}
const ssf = await SafeSingletonFactory.init(userWallet);

return {
factory: SafeProxyFactory__factory.connect(
SAFE_FACTORY_ADDRESS,
userWallet,
),
singleton: Safe__factory.connect(SINGLETON_ADDRESS, provider),
factory: await ssf.connectOrDeploy(SafeProxyFactory__factory, []),
singleton: await ssf.connectOrDeploy(Safe__factory, []),
bundlerProvider,
provider,
userWallet,
Expand All @@ -69,25 +60,16 @@ describe("SafeECDSAPlugin", () => {
* 2. Executing a transaction is possible
*/
itif("should pass the ERC4337 validation", async () => {
const {
singleton,
factory,
provider,
bundlerProvider,
userWallet,
entryPoints,
} = await setupTests();
const { singleton, provider, bundlerProvider, userWallet, entryPoints } =
await setupTests();
const ENTRYPOINT_ADDRESS = entryPoints[0];

const safeECDSAPluginFactory = (
await hre.ethers.getContractFactory("SafeECDSAPlugin")
).connect(userWallet);
const safeECDSAPlugin = await safeECDSAPluginFactory.deploy(
ENTRYPOINT_ADDRESS,
userWallet.address,
{ gasLimit: 1_000_000 },
const ssf = await SafeSingletonFactory.init(userWallet);

const safeECDSAFactory = await ssf.connectOrDeploy(
SafeECDSAFactory__factory,
[],
);
await safeECDSAPlugin.deploymentTransaction()?.wait();

const feeData = await provider.getFeeData();
if (!feeData.maxFeePerGas || !feeData.maxPriorityFeePerGas) {
Expand All @@ -99,64 +81,50 @@ describe("SafeECDSAPlugin", () => {
const maxFeePerGas = `0x${feeData.maxFeePerGas.toString()}`;
const maxPriorityFeePerGas = `0x${feeData.maxPriorityFeePerGas.toString()}`;

const safeECDSAPluginAddress = await safeECDSAPlugin.getAddress();
const singletonAddress = await singleton.getAddress();
const factoryAddress = await factory.getAddress();

const moduleInitializer =
safeECDSAPlugin.interface.encodeFunctionData("enableMyself");
const encodedInitializer = singleton.interface.encodeFunctionData("setup", [
[userWallet.address],
1,
safeECDSAPluginAddress,
moduleInitializer,
safeECDSAPluginAddress,
AddressZero,
const owner = ethers.Wallet.createRandom();

const createArgs = [
singleton,
ENTRYPOINT_ADDRESS,
owner.address,
0,
AddressZero,
]);

const deployedAddress = await calculateProxyAddress(
factory,
singletonAddress,
encodedInitializer,
73,
] satisfies Parameters<typeof safeECDSAFactory.create.staticCall>;

const accountAddress = await safeECDSAFactory.create.staticCall(
...createArgs,
);

// The initCode contains 20 bytes of the factory address and the rest is the
// calldata to be forwarded
const initCode = concat([
factoryAddress,
factory.interface.encodeFunctionData("createProxyWithNonce", [
singletonAddress,
encodedInitializer,
73,
]),
]);

const signer = new ethers.Wallet(
await receiptOf(safeECDSAFactory.create(...createArgs));

const recipient = new ethers.Wallet(
"0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d",
);
const recipientAddress = signer.address;
const transferAmount = ethers.parseEther("1");

const userOpCallData = safeECDSAPlugin.interface.encodeFunctionData(
"execTransaction",
[recipientAddress, transferAmount, "0x00"],
);
const userOpCallData =
SafeECDSAPlugin__factory.createInterface().encodeFunctionData(
"execTransaction",
[recipient.address, transferAmount, "0x00"],
);

// Native tokens for the pre-fund 💸
await receiptOf(
userWallet.sendTransaction({
to: deployedAddress,
to: accountAddress,
value: ethers.parseEther("100"),
}),
);

const unsignedUserOperation: UserOperationStruct = {
sender: deployedAddress,
sender: accountAddress,
nonce: "0x0",
initCode,

// Note: initCode is not used because we need to create both the safe
// proxy and the plugin, and 4337 currently only allows one contract
// creation in this step. Since we need an extra step anyway, it's simpler
// to do the whole create outside of 4337.
initCode: "0x",

callData: userOpCallData,
callGasLimit: "0x7A120",
verificationGasLimit: "0x7A120",
Expand All @@ -171,9 +139,9 @@ describe("SafeECDSAPlugin", () => {
const userOpHash = getUserOpHash(
resolvedUserOp,
ENTRYPOINT_ADDRESS,
Number(provider._network.chainId),
Number((await provider.getNetwork()).chainId),
);
const userOpSignature = await userWallet.signMessage(getBytes(userOpHash));
const userOpSignature = await owner.signMessage(getBytes(userOpHash));

const userOperation = {
...unsignedUserOperation,
Expand All @@ -190,11 +158,11 @@ describe("SafeECDSAPlugin", () => {
// `;
// console.log(DEBUG_MESSAGE);

const recipientBalanceBefore = await provider.getBalance(recipientAddress);
const recipientBalanceBefore = await provider.getBalance(recipient.address);

await sendUserOpAndWait(userOperation, ENTRYPOINT_ADDRESS, bundlerProvider);

const recipientBalanceAfter = await provider.getBalance(recipientAddress);
const recipientBalanceAfter = await provider.getBalance(recipient.address);

const expectedRecipientBalance = recipientBalanceBefore + transferAmount;
expect(recipientBalanceAfter).to.equal(expectedRecipientBalance);
Expand Down
Loading