I have set up a local Ethereum entrypoint instance and a bundler using eth-infinitism/bundler with a SimpleAccount and SimpleAccountFactory contract deployed at the ACCOUNT_FACTORY address.
Setup Steps
Following the README instructions, I initialized my environment as follows:
yarn && yarn preprocess- Deploy contracts:
yarn hardhat-deploy --network localhost - Start bundler:
yarn run bundler --unsafe(hardhat nodeenvironment) - Deploy account factory:
yarn run runop --deployFactory --network http://127.0.0.1:8545/ --entryPoint 0x0000000071727De22E5E9d8BAf0edAc6f37da032
Goal
I am attempting to:
- Create a smart account.
- Deploy an ERC721 NFT contract from the smart account, owned by the smart account.
- Mint an NFT from the deployed contract to the smart account.
For this, I wrote deployNFTFromSmart.ts below using the account-abstraction SDK at "packages/bundler/src/runner/deployNFTFromSmart.ts" and put it near runop.ts mentioned above.
import { ethers } from 'hardhat' import { Command } from 'commander' import { SimpleAccountAPI, HttpRpcClient } from '@account-abstraction/sdk' import { SimpleAccountFactory__factory } from '@account-abstraction/utils' import { getNetworkProvider } from '../Config' import { formatEther } from 'ethers/lib/utils' const ENTRY_POINT = '0x0000000071727De22E5E9d8BAf0edAc6f37da032' const ACCOUNT_FACTORY = '0x899A1E06fE947DE2114EE36fa8707f57bcA476e7' const main = async (): Promise<void> => { const program = new Command() .version('0.0.1') .option('--network <string>', 'network name or url', 'http://localhost:8545') .option('--accountFactory <string>', 'address of the SimpleAccountFactory contract', ACCOUNT_FACTORY) .option('--entryPoint <string>', 'address of the supported EntryPoint contract', ENTRY_POINT) .option('--show-stack-traces', 'Show stack traces.') const opts = program.parse().opts() const provider = getNetworkProvider(opts.network) const signer = provider.getSigner() // Get the smart account addresses first const accountFactory = SimpleAccountFactory__factory.connect(opts.accountFactory!, signer) const account1 = new ethers.Wallet('0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d', provider) const smartAccount1Address = await accountFactory.getAddress(account1.address, 0) console.log('Smart Account1 address:', smartAccount1Address) // 1. Deploy the NFT contract using UserOp console.log('\nDeploying NFT contract via SmartAccount...') const TestNFTFactory = await ethers.getContractFactory('TestNFT', signer) // Create deployment bytecode and pass smart account as owner to constructor const deployTx = await TestNFTFactory.getDeployTransaction() // We need to use SimpleAccount.execute() not SimpleAccountFactory // Get the SimpleAccount contract interface const SimpleAccount = await ethers.getContractFactory('SimpleAccount') const deployData = SimpleAccount.interface.encodeFunctionData( 'execute', [ ethers.constants.AddressZero, // target zero address for deployment 0, // no ETH value deployTx.data! // deployment bytecode ] ) // 2. Setup bundler client const bundlerUrl = 'http://localhost:3000/rpc' const chainId = await provider.getNetwork().then(net => net.chainId) const bundlerClient = new HttpRpcClient(bundlerUrl, opts.entryPoint, chainId) // 3. Setup SimpleAccountAPI const api = new SimpleAccountAPI({ provider, entryPointAddress: opts.entryPoint, owner: account1, factoryAddress: opts.accountFactory }) try { // 4. Create UserOp for deployment console.log('\nCreating deployment UserOperation...') const deployOp = await api.createSignedUserOp({ target: ethers.constants.AddressZero, // target zero address for contract creation data: deployData, maxFeePerGas: await provider.getGasPrice(), maxPriorityFeePerGas: await provider.getGasPrice() }) // 5. Send deployment operation console.log('Sending deployment operation...') const userOpHash = await bundlerClient.sendUserOpToBundler(deployOp) console.log('UserOperation hash:', userOpHash) // 6. Wait for transaction to be mined console.log('Waiting for transaction to be mined...') const receipt = await api.getUserOpReceipt(userOpHash) if (!receipt) { throw new Error('Timeout waiting for transaction receipt') } // Get the deployed contract address via create2 const nftContractAddress = ethers.utils.getCreate2Address( smartAccount1Address, ethers.utils.solidityKeccak256(['bytes'], [deployTx.data!]), ethers.utils.solidityKeccak256(['bytes'], [deployTx.data!]) ) const nftContract = TestNFTFactory.attach(nftContractAddress) // Verify deployment const ownerOfContract = await nftContract.owner() console.log('NFT Contract deployed to:', nftContractAddress) console.log('NFT Contract owner:', ownerOfContract) // 7. Create mint operation console.log('\nCreating mint operation...') const mintOp = await api.createSignedUserOp({ target: nftContractAddress, data: nftContract.interface.encodeFunctionData('mint', [smartAccount1Address]), maxFeePerGas: await provider.getGasPrice(), maxPriorityFeePerGas: await provider.getGasPrice() }) // 8. Send mint operation console.log('Sending mint operation...') const mintUserOpHash = await bundlerClient.sendUserOpToBundler(mintOp) console.log('UserOperation hash:', mintUserOpHash) // 9. Wait for transaction to be mined console.log('Waiting for transaction to be mined...') const mintReceipt = await api.getUserOpReceipt(mintUserOpHash) if (!mintReceipt) { throw new Error('Timeout waiting for mint transaction receipt') } // 10. Check if mint was successful const owner = await nftContract.ownerOf(0) if (owner.toLowerCase() === smartAccount1Address.toLowerCase()) { console.log('NFT minted successfully!') console.log('Token owner:', owner) } else { console.log('Mint failed or token owned by different address:', owner) } } catch (error: any) { console.error('Error:', error.message) // Print debug info const balance = await provider.getBalance(smartAccount1Address) console.log('Smart account balance:', formatEther(balance)) const nonce = await api.getNonce() console.log('Smart account nonce:', nonce.toString()) } } main().catch(console.error) The contract TestNFT.sol is a very simple public mint ERC721:
// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; contract TestNFT is ERC721, Ownable { uint256 public nextTokenId; constructor() ERC721("TestNFT", "TNFT") Ownable(msg.sender) {} function mint(address to) public { _safeMint(to, nextTokenId); nextTokenId++; } } Script Execution
I executed:
yarn run ts-node src/runner/deployNFTFromSmart.ts --network http://127.0.0.1:8545 --entryPoint 0x0000000071727De22E5E9d8BAf0edAc6f37da032 but encountered the following error:
Error: call revert exception [ See: https://links.ethers.org/v5-errors-CALL_EXCEPTION ] (method="owner()", data="0x", errorArgs=null, errorName=null, errorSignature=null, reason=null, code=CALL_EXCEPTION, version=abi/5.7.0) Here is the full DeployNFTFromSmart.ts script logs:
url= http://127.0.0.1:8545 Smart Account1 address: 0x4D755B7ad451809FC0dec877f410872c9Ca580b1 Deploying NFT contract via SmartAccount... Creating deployment UserOperation... Sending deployment operation... UserOperation hash: 0x9e0a53880d98c021d6a41c060f68becc65e089707d8b44765d51fca311688a3d Waiting for transaction to be mined... Error: call revert exception [ See: https://links.ethers.org/v5-errors-CALL_EXCEPTION ] (method="owner()", data="0x", errorArgs=null, errorName=null, errorSignature=null, reason=null, code=CALL_EXCEPTION, version=abi/5.7.0) Smart account balance: 0.996271620544313477 Smart account nonce: 14 ✨ Done in 16.00s. What I have checked
- The smart account has sufficient gas (balance: 0.999 ETH).
- The SimpleAccountFactory and bundler are working correctly (I can execute other simple transactions such as ETH transfer between smart accounts).
Observed Behavior
- The UserOp creation is successful and returns a
UserOperationhash. - However, deployment transaction fails, possibly due to the ownership field is not set.
Possible Cause
- I am trying to use the
execute()function ofSimpleAccount.solto deploy the NFT contract. However, the sender field in the NFT contract constructor is not properly set, resulting in theowner()function failing.
Question
- How can I modify the NFT contract deployment above to ensure the Smart Account is set as the contract owner?
- Is there a recommended way to deploy contracts using
execute()so that NFT contract deployment is similar to that of EOA accounts with ownerships?
Any guidance or references would be greatly appreciated. Thanks in advance!