Skip to content

Commit f8bd215

Browse files
committed
Remove gasleft in setupModules, add erc4337 compatibility test
1 parent ad9b319 commit f8bd215

8 files changed

Lines changed: 238 additions & 5 deletions

File tree

.env.sample

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,8 @@ INFURA_KEY=""
44
# Used for custom network
55
NODE_URL=""
66
ETHERSCAN_API_KEY=""
7+
# (Optional) Used to run ERC-4337 compatibility test. MNEMONIC is also required.
8+
ERC4337_TEST_BUNDLER_URL=
9+
ERC4337_TEST_NODE_URL=
10+
ERC4337_TEST_SINGLETON_ADDRESS=
11+
ERC4337_TEST_SAFE_FACTORY_ADDRESS=

README.md

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,29 @@ Usage
1515
yarn
1616
```
1717

18-
### Run all tests:
18+
### Testing
19+
20+
To run the tests:
1921

2022
```bash
2123
yarn build
2224
yarn test
2325
```
2426

27+
Optionally, if you want to run the ERC-4337 compatibility test, it uses a live bundler and node, so it contains some pre-requisites:
28+
29+
1. Define the environment variables:
30+
31+
```
32+
ERC4337_TEST_BUNDLER_URL=
33+
ERC4337_TEST_NODE_URL=
34+
ERC4337_TEST_SINGLETON_ADDRESS=
35+
ERC4337_TEST_SAFE_FACTORY_ADDRESS=
36+
MNEMONIC=
37+
```
38+
39+
2. Pre-fund the executor account derived from the mnemonic with some Native Token to cover the deployment of an ERC4337 module and the pre-fund of the Safe for the test operation.
40+
2541
### Deployments
2642

2743
A collection of the different Safe contract deployments and their addresses can be found in the [Safe deployments](https://github.com/safe-global/safe-deployments) repository.

contracts/base/ModuleManager.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ abstract contract ModuleManager is SelfAuthorized, Executor {
3535
if (to != address(0)) {
3636
require(isContract(to), "GS002");
3737
// Setup has to complete successfully or transaction fails.
38-
require(execute(to, 0, data, Enum.Operation.DelegateCall, gasleft()), "GS000");
38+
require(execute(to, 0, data, Enum.Operation.DelegateCall, type(uint256).max), "GS000");
3939
}
4040
}
4141

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
// SPDX-License-Identifier: LGPL-3.0-only
2+
pragma solidity >=0.7.0 <0.9.0;
3+
pragma abicoder v2;
4+
5+
import "../../libraries/SafeStorage.sol";
6+
7+
struct UserOperation {
8+
address sender;
9+
uint256 nonce;
10+
bytes initCode;
11+
bytes callData;
12+
uint256 callGasLimit;
13+
uint256 verificationGasLimit;
14+
uint256 preVerificationGas;
15+
uint256 maxFeePerGas;
16+
uint256 maxPriorityFeePerGas;
17+
bytes paymasterAndData;
18+
bytes signature;
19+
}
20+
21+
interface ISafe {
22+
function execTransactionFromModule(address to, uint256 value, bytes memory data, uint8 operation) external returns (bool success);
23+
}
24+
25+
/// @dev A Dummy 4337 Module/Handler for testing purposes
26+
/// ⚠️ ⚠️ ⚠️ DO NOT USE IN PRODUCTION ⚠️ ⚠️ ⚠️
27+
/// The module does not perform ANY validation, it just executes validateUserOp and execTransaction
28+
/// to perform the opcode level compliance by the bundler.
29+
contract Test4337ModuleAndHandler is SafeStorage {
30+
address public immutable myAddress;
31+
address public immutable entryPoint;
32+
33+
address internal constant SENTINEL_MODULES = address(0x1);
34+
35+
constructor(address entryPointAddress) {
36+
entryPoint = entryPointAddress;
37+
myAddress = address(this);
38+
}
39+
40+
function validateUserOp(UserOperation calldata userOp, bytes32, uint256 missingAccountFunds) external returns (uint256 validationData) {
41+
address payable safeAddress = payable(userOp.sender);
42+
ISafe senderSafe = ISafe(safeAddress);
43+
44+
if (missingAccountFunds != 0) {
45+
senderSafe.execTransactionFromModule(entryPoint, missingAccountFunds, "", 0);
46+
}
47+
48+
return 0;
49+
}
50+
51+
function execTransaction(address to, uint256 value, bytes calldata data) external payable {
52+
address payable safeAddress = payable(msg.sender);
53+
ISafe safe = ISafe(safeAddress);
54+
require(safe.execTransactionFromModule(to, value, data, 0), "tx failed");
55+
}
56+
57+
function enableMyself() public {
58+
require(myAddress != address(this), "You need to DELEGATECALL, sir");
59+
60+
// Module cannot be added twice.
61+
require(modules[myAddress] == address(0), "GS102");
62+
modules[myAddress] = modules[SENTINEL_MODULES];
63+
modules[SENTINEL_MODULES] = myAddress;
64+
}
65+
}

hardhat.config.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import "@nomiclabs/hardhat-ethers";
12
import type { HardhatUserConfig, HttpNetworkUserConfig } from "hardhat/types";
23
import "@nomiclabs/hardhat-etherscan";
34
import "@nomiclabs/hardhat-waffle";
@@ -41,7 +42,7 @@ import { BigNumber } from "@ethersproject/bignumber";
4142
import { DeterministicDeploymentInfo } from "hardhat-deploy/dist/types";
4243

4344
const primarySolidityVersion = SOLIDITY_VERSION || "0.7.6";
44-
const soliditySettings = !!SOLIDITY_SETTINGS ? JSON.parse(SOLIDITY_SETTINGS) : undefined;
45+
const soliditySettings = SOLIDITY_SETTINGS ? JSON.parse(SOLIDITY_SETTINGS) : undefined;
4546

4647
const deterministicDeployment = (network: string): DeterministicDeploymentInfo => {
4748
const info = getSingletonFactoryInfo(parseInt(network));
@@ -132,7 +133,7 @@ const userConfig: HardhatUserConfig = {
132133
},
133134
};
134135
if (NODE_URL) {
135-
userConfig.networks!!.custom = {
136+
userConfig.networks!.custom = {
136137
...sharedNetworkConfig,
137138
url: NODE_URL,
138139
};
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import hre from "hardhat";
2+
import { expect } from "chai";
3+
import { AddressZero } from "@ethersproject/constants";
4+
import { hexConcat } from "ethers/lib/utils";
5+
import { getFactoryContract, getSafeSingletonContract } from "../utils/setup";
6+
import { calculateProxyAddress } from "../../src/utils/proxies";
7+
8+
const ERC4337_TEST_ENV_VARIABLES_DEFINED =
9+
typeof process.env.ERC4337_TEST_BUNDLER_URL !== "undefined" &&
10+
typeof process.env.ERC4337_TEST_NODE_URL !== "undefined" &&
11+
typeof process.env.ERC4337_TEST_SAFE_FACTORY_ADDRESS !== "undefined" &&
12+
typeof process.env.ERC4337_TEST_SINGLETON_ADDRESS !== "undefined" &&
13+
typeof process.env.MNEMONIC !== "undefined";
14+
15+
const itif = ERC4337_TEST_ENV_VARIABLES_DEFINED ? it : it.skip;
16+
const SAFE_FACTORY_ADDRESS = process.env.ERC4337_TEST_SAFE_FACTORY_ADDRESS;
17+
const SINGLETON_ADDRESS = process.env.ERC4337_TEST_SINGLETON_ADDRESS;
18+
const BUNDLER_URL = process.env.ERC4337_TEST_BUNDLER_URL;
19+
const NODE_URL = process.env.ERC4337_TEST_NODE_URL;
20+
const MNEMONIC = process.env.MNEMONIC;
21+
22+
type UserOperation = {
23+
sender: string;
24+
nonce: string;
25+
initCode: string;
26+
callData: string;
27+
callGasLimit: string;
28+
verificationGasLimit: string;
29+
preVerificationGas: string;
30+
maxFeePerGas: string;
31+
maxPriorityFeePerGas: string;
32+
paymasterAndData: string;
33+
signature: string;
34+
};
35+
36+
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
37+
38+
describe("Safe.ERC4337", () => {
39+
const setupTests = async () => {
40+
const factory = await getFactoryContract();
41+
const singleton = await getSafeSingletonContract();
42+
const bundlerProvider = new hre.ethers.providers.JsonRpcProvider(BUNDLER_URL);
43+
const provider = new hre.ethers.providers.JsonRpcProvider(NODE_URL);
44+
const userWallet = hre.ethers.Wallet.fromMnemonic(MNEMONIC as string).connect(provider);
45+
46+
const entryPoints = await bundlerProvider.send("eth_supportedEntryPoints", []);
47+
if (entryPoints.length === 0) {
48+
throw new Error("No entry points found");
49+
}
50+
51+
return {
52+
factory: factory.attach(SAFE_FACTORY_ADDRESS).connect(userWallet),
53+
singleton: singleton.attach(SINGLETON_ADDRESS).connect(provider),
54+
bundlerProvider,
55+
provider,
56+
userWallet,
57+
entryPoints,
58+
};
59+
};
60+
61+
/**
62+
* This test verifies the ERC4337 based on gas estimation for a user operation
63+
* The user operation deploys a Safe with the ERC4337 module and a handler
64+
* and executes a transaction, thus verifying two things:
65+
* 1. Deployment of the Safe with the ERC4337 module and handler is possible
66+
* 2. Executing a transaction is possible
67+
*/
68+
itif("should pass the ERC4337 validation", async () => {
69+
const { singleton, factory, provider, bundlerProvider, userWallet, entryPoints } = await setupTests();
70+
const ENTRYPOINT_ADDRESS = entryPoints[0];
71+
72+
const erc4337ModuleAndHandlerFactory = (await hre.ethers.getContractFactory("Test4337ModuleAndHandler")).connect(userWallet);
73+
const erc4337ModuleAndHandler = await erc4337ModuleAndHandlerFactory.deploy(ENTRYPOINT_ADDRESS);
74+
// The bundler uses a different node, so we need to allow it sometime to sync
75+
await sleep(10000);
76+
77+
const feeData = await provider.getFeeData();
78+
const maxFeePerGas = feeData.maxFeePerGas.toHexString();
79+
80+
const maxPriorityFeePerGas = feeData.maxPriorityFeePerGas.toHexString();
81+
82+
const moduleInitializer = erc4337ModuleAndHandler.interface.encodeFunctionData("enableMyself", []);
83+
const encodedInitializer = singleton.interface.encodeFunctionData("setup", [
84+
[userWallet.address],
85+
1,
86+
erc4337ModuleAndHandler.address,
87+
moduleInitializer,
88+
erc4337ModuleAndHandler.address,
89+
AddressZero,
90+
0,
91+
AddressZero,
92+
]);
93+
const deployedAddress = await calculateProxyAddress(factory, singleton.address, encodedInitializer, 73);
94+
95+
// The initCode contains 20 bytes of the factory address and the rest is the calldata to be forwarded
96+
const initCode = hexConcat([
97+
factory.address,
98+
factory.interface.encodeFunctionData("createProxyWithNonce", [singleton.address, encodedInitializer, 73]),
99+
]);
100+
const userOpCallData = erc4337ModuleAndHandler.interface.encodeFunctionData("execTransaction", [userWallet.address, 0, 0]);
101+
102+
// Native tokens for the pre-fund 💸
103+
await userWallet.sendTransaction({ to: deployedAddress, value: hre.ethers.utils.parseEther("0.001") });
104+
// The bundler uses a different node, so we need to allow it sometime to sync
105+
await sleep(10000);
106+
107+
const userOperation: UserOperation = {
108+
sender: deployedAddress,
109+
nonce: "0x0",
110+
initCode,
111+
callData: userOpCallData,
112+
callGasLimit: "0x7A120",
113+
verificationGasLimit: "0x7A120",
114+
preVerificationGas: "0x186A0",
115+
maxFeePerGas,
116+
maxPriorityFeePerGas,
117+
paymasterAndData: "0x",
118+
signature: "0x",
119+
};
120+
121+
const DEBUG_MESSAGE = `
122+
Using entry point: ${ENTRYPOINT_ADDRESS}
123+
Deployed Safe address: ${deployedAddress}
124+
Module/Handler address: ${erc4337ModuleAndHandler.address}
125+
User operation:
126+
${JSON.stringify(userOperation, null, 2)}
127+
`;
128+
console.log(DEBUG_MESSAGE);
129+
130+
const estimatedGas = await bundlerProvider.send("eth_estimateUserOperationGas", [userOperation, ENTRYPOINT_ADDRESS]);
131+
expect(estimatedGas).to.not.be.undefined;
132+
});
133+
});

test/utils/setup.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,18 @@ export const getSafeSingleton = async () => {
2828
return Safe.attach(SafeDeployment.address);
2929
};
3030

31+
export const getSafeSingletonContract = async () => {
32+
const safeSingleton = await hre.ethers.getContractFactory(safeContractUnderTest());
33+
34+
return safeSingleton;
35+
};
36+
37+
export const getFactoryContract = async () => {
38+
const factory = await hre.ethers.getContractFactory("SafeProxyFactory");
39+
40+
return factory;
41+
};
42+
3143
export const getFactory = async () => {
3244
const FactoryDeployment = await deployments.get("SafeProxyFactory");
3345
const Factory = await hre.ethers.getContractFactory("SafeProxyFactory");

tsconfig.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,6 @@
1818
"resolveJsonModule": true
1919
},
2020
"exclude": ["dist", "node_modules"],
21-
"include": ["./src/index.ts", "./types"]
21+
"include": ["./src/index.ts", "./types"],
22+
"files": ["./hardhat.config.ts"]
2223
}

0 commit comments

Comments
 (0)