BatchedWallet is a simple implementation of an ERC-4337-compliant smart wallet, additionally it complies with the ERC-1271 (contract signing) and is also upgradable. For a complete outline of the ERC-4337 spec please read the EIP here.
- Design Decisions
- Improvements
- Getting Started
- Deploying to an EVM Testnet
- Sepolia Deployment Addresses
- Interacting with the Testnet Deployment
- Security
- Contributing
- Thank You!
The BatchedWallet design is focused on simplicity while still including the most essential features for an ERC-4337-compliant smart wallet. Here are some of the considerations that were made:
- The wallet supports both EOA (ECDSA signatures) and smart contract signing (ERC-1271 compliant).
- For simplicity the wallet currently only supports one owner address (this obviously limits the potential use-cases markedly).
- The wallet is upgradable, as recommended by the EIP-4337 spec.
- The EntryPoint address is hard-coded into the BatchedWallet for gas efficiency, thus for EntryPoint upgrades (if the EntryPoint address changes) redeployment would be required.
There are many areas where the BatchedWallet contracts can be improved. The list below touches on some of these areas:
- Expand the ownership options to include multiple owner and/or tiers of ownership (a privileges system).
- Implement a testing setup that uses an alternative mempool (to more closely replicate a production environment).
- Improve the signature encoding scheme to include additional details such as
chainIdas well as asigTypeas a part of the encoding. - Expand testing of the signature verification scheme.
- Add checks in the existing testing for event emissions using the Foundry cheatcodes.
- Add testing related to ERC-1155 and ERC-721 tokens.
- Further gas optimisation of wallet functions.
- Abstract out authorization functionalities from the BatchedWallet contract into a seperate Auth-specific contract.
- Make the scripts adaptable for EVM-based chains other than the Sepolia testnet.
- Add the option to have custom "validators" for validating different types of operations on the wallet.
- Enable customization of default fallback functionality by allowing
STATICCALLs to a custom contract (without allowing state modifications).
Please install the following:
- Foundry / Foundryup
- Make
- Solhint (ensure that you have it installed globally)
- Python (if you want to use Slither)
- Slither (optional)
git clone https://github.com/leopoldjoy/simple-smart-wallet
cd simple-smart-wallet
forge install
forge testforge test
The repo is currently setup to deploy to the Sepolia Ethereum testnet, we will walk through the setup process below.
You'll need to add the following variables to a .env file in the root of the repo:
PRIVATE_KEY: A private key from your wallet. (If you don't have one you can get a private key from a new Metamask account.)SEPOLIA_RPC_URL: A URL to connect to the blockchain. You can get one for free from Infura accountSEPOLIA_VERIFIER_URL: The URL of the Etherescan Sepolia endpoint, currently this should be:https://api-sepolia.etherscan.io/apiETHERSCAN_API_KEY: Your Etherscan API key, you can sign up for a free account here.
If you prefer, you can also run the following command from the project root to create the .env file and then input your values manually:
cp .env.example .env
Please note that there is an existing default private key included in the .env.example file. This key was used to deploy the Sepolia Deployment Addresses in order to streamline testing if you prefer not to run your own deployment (this address is the owner of the deployed Sepolia contracts).
Since we're using the Sepolia testnet, go get some testnet sepolia ETH if you don't have any already.
We will walk through deploying all of the contracts now, including the mock contracts (for testing purposes). Please do note however that in a real use-case (e.g. in production) the bundler would gather UserOperations from an alternative mempool, however for our testing purposes we will be relaying our UserOperations to our mock bundler in the same general mempool of the testnet.
First run the following in order to use the environment variables we set:
source .env
Deploy the ERC20Mock contract (so we can use it to test ERC-20 transfers):
make deploy-sepolia contract=ERC20Mock
Deploy the BundlerMock contract (which will function as our ERC-4337 bundler for testing purposes):
make deploy-sepolia contract=BundlerMock
Deploy the BatchedWalletFactory contract:
make deploy-sepolia contract=BatchedWalletFactory
Now we create a BatchedWallet contract:
OWNER_ADDRESS=<your-wallet-address> \
FACTORY_ADDRESS=<the-address-printed-from-the-BatchedWalletFactory-deployment-above> \
SALT=<a-bytes32-message-totaling-66-characters> \
make deploy-sepolia contract=BatchedWallet
Please replace the poritions above surrounded by arrows with the relevent values. (Note that these environment variables may also be set via the .env file if you prefer, however this is not recommended since modifications would need to be made before each deployment command.) For the SALT value, please ensure that is has the correct amount of trailing zeros, totaling 66 characters (for example: 0x7465737400000000000000000000000000000000000000000000000000000000).
Now we create a SponsorshipPaymaster contract (which will function for testing out paymaster sponsorship functionality):
OWNER_ADDRESS=<your-wallet-address> \
FACTORY_ADDRESS=<the-address-printed-from-the-BatchedWalletFactory-deployment-above> \
make deploy-sepolia contract=SponsorshipPaymaster
If you visit the deployment addresses on Sepolia's Etherscan you may notice that they are also verified. However if any don't verifiy automatically for any reason, simply run the forge verify-contract command (see the documentation here). Also please note that to verify the BatchedWallet contract in particular, since it's deployed via a proxy, you must click the "Is this a proxy?" button in Etherscan and follow the instructions.
Congratulations! We now have all of the contracts deployed that we need to start testing everything out on the testnet!
An existing deployment of the contracts (11/12/23) has been made at the following addresses:
- ERC20Mock: 0xFbFe85108EdE87fdF9933B619311eeac313E31a3
- BundlerMock: 0x7192ff565893d812b0d76de7101eae6fd12e587a
- BatchedWalletFactory: 0xdd4195dae1326a2391714b7fdb67f6d592c21ad6
- BatchedWallet: 0x4788037629494dd2ebb0b665e2027091f1109d56
- SponsorshipPaymaster: 0xc87ebf920b44c8ebf69260b54f2accbe75a9ea81
Also, please note that this is the address of the current EntryPoint deployment on Sepolia: 0x0576a174D229E3cFA37253523E645A78A0C91B57
For the purpose of this walkthrough of the available functionality, we will use the deployment addresses listed in the section above, however feel free to subsitute the contract addresses with your own (if you've completed the deployment steps listed earlier).
Also, if you are using the addresses listed above, please ensure that you also ran cp .env.example .env earlier in the Setup section, since the existing default private key in the .env.example file is the owner of the Sepolia contracts above in order to streamline testing (if you prefer not to run your own deployment). Also, in this case please additionally import this private key into your MetaMask wallet (so we can test with it in the following steps).
In order for our SponsorshipPaymaster contract to cover the cost of UserOperations, we must make a deposit into the EntryPoint on behalf of the paymaster contract's address. Go to the EntryPoint address, connect your Web3 wallet, and then click the depositTo() function. Enter the following values:
payableAmount (ether):0.2(or whatever amount you want to deposit on behalf of the paymaster)account: 0xc87ebf920b44c8ebf69260b54f2accbe75a9ea81(the SponsorshipPaymaster address from the deployment)
Submit the transaction and wait for it to confirm. You can also switch to the read-tab of the contract and call the deposits() function with the paymaster's address to confirm the new size of the paymaster's deposit on the EntryPoint.
Go to the ERC20Mock contract and run the mint() function with the following values:
account:0x4788037629494dd2ebB0b665E2027091F1109d56(the deployed BatchedWallet's address)amount:1000000000000000000(1 token)
Submit and wait for confirmation. The BatchedWallet now has 1 token worth of the ERC20Mock token.
First we need to sign the UserOperation that we want to submit to the Bundler. To do this we must run the following script:
USEROP_SENDER=0x4788037629494dd2ebB0b665E2027091F1109d56 \
USEROP_NONCE=2 \
USEROP_CALL_DATA_ADDRESS=0xFbFe85108EdE87fdF9933B619311eeac313E31a3 \
USEROP_CALL_DATA_ERC20_TO_ADDRESS=0x227C8be27B6699747b5a33F623E65eA072a6153A \
USEROP_CALL_DATA_ERC20_AMOUNT=1000000000000000000 \
USEROP_PAYMASTER_ADDRESS=0xC87eBf920b44C8eBf69260b54F2AccBE75a9EA81 \
make sign-user-op
Note that you will need to replace the nonce (and possibly the other values depending on if you deployed yourself), you can find the full documentation for these environment variables here. When the command finishes, take note of the resulting signature and all of the returned values. Then go to the bundler, and run the post() function with the following values:
entryPoint:0x0576a174D229E3cFA37253523E645A78A0C91B57sender:0x4788037629494dd2ebB0b665E2027091F1109d56(the BatchedWallet address)nonce:2(be sure that this matches the correct value and the same value you used to create the signature above)initCode:0xcallData:<insert-from-the-result-above>callGasLimit:100000verificationGasLimit:100000preVerificationGas:10000maxFeePerGas:2000000000maxPriorityFeePerGas:2000000000paymasterAndData:0xc87ebf920b44c8ebf69260b54f2accbe75a9ea81signature:<insert-from-the-result-above>
Please note that it's essential that the values that you used to generate the signature are the same as the values passed above, otherwise the transaction will fail.
Submit the transaction and await confirmation. Once confirmed, you can view the state changes in Etherscan and confirm that everything happened correctly (e.g. the paymaster covered the UserOperation cost and the tokens were transferred). You can see an example of the state changes of a confirmed transaction here.
To run slither, use:
make slither
And get your slither output.
Please be sure to run the linter before pushing:
make lint