The transition from EOA to Smart Contract Account (SCA) is a huge win for users, but what does it mean for developers? Having a more versatile account is great — yay for everyone building passkey multisigs, session keys, and modules — but it is useless if no dapp knows how to interact with it. This article lays out some useful things to think about when building for the new account type, and introduces the Permissionlesss toSmartAccount
helper.
A brief look at the current transaction flow for an EOA is a useful place to start, since if our SCA can maintain a similar flow, then both account types can exist in parallel while the transition happens. This saves a lot of code from being rewritten as we can keep using existing hooks in Wagmi like sendTransaction
.
When a user on a dapp decides to take some onchain action, a transaction ({to
, value
, data
...}) is formatted and a wallet is requested to get a signature on that transaction data. The wallet will then (in the case of a "local account") ask some secure store for access to the private key which could be stored on your browser, device, or keychain. If you have permission (by passing a biometrics or a password check) then you get a signature on the transaction, and that signed transaction can be sent onchain.
With a smart account we can define much more flexible logic related to signature validation or transaction execution, but this means we have to extend the basic Viem account API (seen above) to handle that. Below we have a very simple example of the interface an ERC-4337 contract might have, and some important considerations when designing the account.
First thing to note is initialize
. This exists because unlike an EOA, the SCA will eventually need to be deployed on each chain it wants to operate on. Generally each type of SCA will have a corresponding "factory" contract which can deploy copies of it. With that in mind we can define a few useful methods that we need to add to our previous Viem account to convert it to a smart account.
getFactory
: Get the address that will deploy our account.getFactoryData
: Data that the factory will be called with (maybe to set an owner address on the account, or give the contract a name)getInitCode
: Combines the above two things into one.
Next we have validateUserOp
which allows us to define custom validation logic on our account - where userOp
is the new transaction object for ERC-4337 smart accounts. This custom validation is usually in a function called _isValidSignature
which can also be used for ERC-1271 contract signatures. Because of this we have to adapt our existing signature methods on the Viem account.
signMessage
&signTypedData
: These need adapted to not just sign the message or data, but to structure the data to match the way the contract implemented it's_isValidSignature
check. Since most ERC-1271 contract signatures adhere to ERC-712, we will wrap the message with some "domain parameters" - things like the verifying contracts address, chain ID, version of contract etc.signTransaction
: This is no longer used as we don't directly sign transactions anymore!signUserOperation
: Sign the new transaction object instead.
Another difference we need to deal with is the fact that the EVM does not increment nonce for an address with code when a transaction occurs (only when that that address creates a new contract). Because of this the EntryPoint
contract, responsible for handling ERC-4337 transactions, keeps track of the nonce of SCA wallets so we add a method to check that.
getNonce
: Call the entrypoint anytime we want our accounts nonce.
One final thing is that since the smart account will make it's call through the execute
function, we still need an EOA somewhere to initiate this. For that, Account Abstraction providers are running 'bundlers' to relay these userOps
onchain, so instead of our transport being a node URL, it will be the URL of whatever bundler we're using. With all that together we can start to see the updated smart account.
Thankfully Permissionless, the open source smart account framework from Pimlico, have great wrappers already put together to make this as easy as possible to do in typescript. They conveniently extend the Viem account with all of these functions and more. On top of all that there are some added utilities like getDummySignature
which help with gas estimates for userOps. With everything in place, we can complete the picture of how the transaction flow for a smart account will look.
Since the flow between application and wallet is identical to that of the first EOA example, it means we can maintain the same code as we have for EOAs! We keep the new formatting and added account functions in our extended Viem account, we keep the same Wagmi hooks for interacting with a wallet, and we let a bundler handle the forwarding of the userOp onchain... so in the end the developer experience for building with SCAs is similar to EOAs, and the end users benefit from a more versatile and secure account.