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

Commit

Permalink
Demonstrate SafeECDSAPlugin in in-page wallet (#97)
Browse files Browse the repository at this point in the history
* Refactor to support different account types

* Import SafeECDSAPlugin via symlink

* Include EntryPoint in deployment

* Add sendEth task and document it for bundler

* Remove unused method

* Add Safe, SafeProxyFactory to deployed contracts

* SafeECDSAFactory, consolidate on non-4337 creation

* Use deterministic addresses instead of config

* SafeECDSAAccountWrapper

* Enable choosing account type
  • Loading branch information
voltrevo authored Sep 20, 2023
1 parent d3eb9c9 commit 78fed87
Show file tree
Hide file tree
Showing 27 changed files with 1,047 additions and 397 deletions.
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 @@ -32,14 +32,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",
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
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,14 @@ import {
} 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 @@ -35,24 +32,16 @@ describe("SafeWebAuthnPlugin", () => {
"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 Down
1 change: 1 addition & 0 deletions demos/inpage/.eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -119,5 +119,6 @@ module.exports = {
'no-continue': 'off',
'no-constant-condition': 'off',
'no-underscore-dangle': 'off',
'consistent-return': 'off',
},
};
Loading

0 comments on commit 78fed87

Please sign in to comment.