Description
Describe the Feature
Problem
I love the simplicity of using etherjs's signer
when interacting with contracts. We built our project libraries and functions relying on it. E.g:
deployPoll(signer: Signer, parameter1: string, ...)
We wanted to implement gasless smart accounts (EIP-7702) using ZeroDev infrastructure. In simple terms: instead of sending our transactions with eth_sendTransaction
we will be sending it with eth_sendUserOperation
to their bundler RPC.
The problem was that we had to change all our functions to work with this new library. So instead of:
const factory = new RandomContractFactory(signer);
const contract = await factory.deploy(5n);
we will be doing something like
const deployCallData = await kernelClient.account.encodeDeployCallData({
abi,
args,
bytecode,
});
const tx = await kernelClient.sendUserOperation({
callData: deployCallData,
sender: kernelClient.account.address,
});
Zerodev tutorial to deploy contracts here.
This mean code duplication and that is bad news.
Current Solution
The Zerodev team created an "adapter" to transform the kernelClient
(a smart account client) to a signer
:
import { KernelEIP1193Provider } from '@zerodev/sdk/providers';
import { ethers } from 'ethers';
// Ensure to initialize your KernelClient here
const kernelClient = …;
// Initialize the KernelEIP1193Provider with your KernelClient
const kernelProvider = new KernelEIP1193Provider(kernelClient);
// Use the KernelProvider with ethers
const ethersProvider = new ethers.BrowserProvider(kernelProvider);
const signer = await ethersProvider.getSigner();
This provider maps every request made from the signer eth_sendTransaction
to a eth_sendUserOperation
. In other terms: it makes you feel you have a local signer of your smart account rather than a normal External Owned Account (EOA). You can check the provider code here.
Side idea: I think ether.js could do integrate this adapter natively so existing projects could use EIP-7702 without changing a lot of their codebase
Interacting with contracts (OK)
Everything works seamlessly when interacting with contracts. E.g:
const contract = RandomContractFactory.connect(contractAddress, signer);
const votes = contract.getVotes(); // RandomContractFactory.sol implements a getVotes() function that returns the votes
...
So no code changes right? Not so fast...
Deploying contracts with factory (FAIL)
I encountered a problem with the factory.deploy()
function because ZeroDev delegates a call to a on-chain contract that executes the code and deploys the transaction itself. I modified the ZeroDev library to bypass this problem and it worked 🥳 . But then I noticed that the contract address set up in the contract was not right:
const factory = new RandomContractFactory(signer);
const contract = await factory.deploy(5n);
const contractAddress = await contract.getAddress();
// it would be an empty "Address" in etherscan and not a "Contract" address
This is caused because the factory code computes the address from the sender and nonce (it is assuming the contract is deployed by an EOA):
ethers.js/src.ts/contract/factory.ts
Line 112 in 0195f44
In these cases, the contract is deployed by another contract so the computed address do not match the deployed contract address.
Solution in ethers.js
I created a small patch for our project that looks like this:
export const getDeployedContractAddress = (receipt: TransactionReceipt): string | undefined => {
const [contractCreationEventTopic] = encodeEventTopics({
abi: [
{
name: "ContractCreation",
type: "event",
inputs: [{ indexed: true, name: "newContract", type: "address" }],
},
],
});
const addr = receipt.logs.find((log) => log.topics[0] === contractCreationEventTopic);
const deployedAddress = addr ? `0x${addr.topics[1]?.slice(addressOffset)}` : undefined;
return deployedAddress;
};
const factory = new RandomContractFactory(signer);
const deployedContract = await factory.deploy(5n);
const { hash } = deployedContract.deploymentTransaction()!;
const receipt = await signer.provider?.getTransactionReceipt(hash);
const address = getDeployedContractAddress(receipt! as unknown as TransactionReceipt);
contract = RandomContractFactory.connect(address!, signer);
I think we could implement this natively in ethers.js by adding a couple of extra things:
-
Set the transaction type
4
oreip-7702
(my reference) in theoverrides
parameter of thefactory.deploy
function:
ethers.js/src.ts/contract/factory.ts
Line 104 in 0195f44
-
If the type is
4
oreip-7702
then wait for the transaction to happen and with the receipt execute the line:
const address = getDeployedContractAddress(receipt! as unknown as TransactionReceipt);
if not then keep the normal flow.
I am assuming that all delegate calls would emit an event similar to event ContractCreation(address indexed newContract);
. If we want to do it more generic we could allow the developer to send the ABI Item that would return the deployed contract address in the overrides
parameter.
Final thoughts
If you think it is a good idea I could open a PR and implement it. I noticed that you have a Discussions section but the only thing related to Account Abstraction or EIP-7702 I found was this issue (and this issue in the Issues section). I thought this solution was straight forward and I decided to open the issue to check for your comments.
Thank you for your time. This became a little more extensive that I thought.
Cheers,
Nico