As Ethereum transitions from Externally Owned Accounts (EOAs) to smart accounts, the potential for actually bringing an improved experience of Dapps to average users becomes increasingly evident.
As the adoption of account abstraction through ERC-4337 gains momentum, a renewed push is underway to enhance the user experience of web3. However, despite the shared goal of advancing both AA and user experience, Dapps have somewhat lagged behind, posing a challenge to facilitating a meaningful transition.
In this handbook, we cast a spotlight on three core domains for dapps to adopt smart wallets and to unleash the complete potential of their interfaces:
Smart Contract wallets are fully supported by the WalletConnect protocol.
However, there are some considerations to be taken when integrating WalletConnect in your dapp for Smart Contract wallets. WalletConnect V2 allows setting specific chains and methods to be specified as required or optional via session namespaces.
Because smart contract wallets may be not deployed on all chains, but deployed on optional chains, you might break the connection if you set a required chain or method in the configuration.
By default, EthereumProvider specifies eth_sendTransaction
and personal_sign
as required methods. For those that want to request additional methods for the session, we recommend passing these through optionalMethods.
import { EthereumProvider } from '@walletconnect/ethereum-provider'
const provider = await EthereumProvider.init({
...
optionalMethods: ["eth_signTypedData"],
...
})
import walletConnectModule from "@web3-onboard/walletconnect";
const wcV2InitOptions = {
...
additionalOptionalMethods: ["eth_signTypedData"],
...
};
const walletConnect = walletConnectModule(wcV2InitOptions);
The recommended config depends on how you designed your interface when a user first connects their wallet.
You let users select the chain before they connect their wallet
Set the required chain id to match the one that the user selected. Pass the rest of the chains your dapp support in the optional field.
ββββimport { EthereumProvider } from '@walletconnect/ethereum-provider'
ββββconst provider = await EthereumProvider.init({
ββββ ...
ββββ})
ββββprovider.connect({
ββββ chains: [10], // user selected optimism
ββββ optionalChains: [1, 137]
ββββ});
ββββimport walletConnectModule from "@web3-onboard/walletconnect";
ββββconst wcV2InitOptions = {
ββββ ...
ββββ requiredChains: [137], // user selected polygon
ββββ optionalChains: [1, 10],
ββββ ...
ββββ};
ββββconst walletConnect = walletConnectModule(wcV2InitOptions);
ββββimport { connect } from '@wagmi/core'
ββββimport { optimism } from '@wagmi/core/chains'
ββββimport { WalletConnectConnector } from '@wagmi/core/connectors/walletConnect'
ββββconst connector = new WalletConnectConnector({
ββββ options: {
ββββ projectId: '...',
ββββ },
ββββ})
ββββconst result = await connect({
ββββ chainId: optimism.id,
ββββ connector,
ββββ})
You let users connect their wallet before they select the chain
Pass all chains your dapp support in optional field.
ββββimport { EthereumProvider } from '@walletconnect/ethereum-provider'
ββββconst provider = await EthereumProvider.init({
ββββ ...
ββββ optionalChains: [1, 10],
ββββ ...
ββββ})
ββββimport walletConnectModule from "@web3-onboard/walletconnect";
ββββconst wcV2InitOptions = {
ββββ ...
ββββ optionalChains: [1, 10],
ββββ ...
ββββ};
ββββconst walletConnect = walletConnectModule(wcV2InitOptions);
Web3Modal and Wagmi currently do no support passing in an empty chains array to EthereumProvider.init.
However, you can show the network selector view and let the user choose the network they want to connect to. They will pass the network as a required chain for the wallet to support.
ββββimport { useWeb3Modal } from '@web3modal/react'
ββββ
ββββconst { open } = useWeb3Modal();
ββββ
ββββopen({ route: 'SelectNetwork' });
Source: https://docs.walletconnect.com/2.0/web3modal/react/wagmi/hooks#useweb3modal
Use the walletconnect required namespace (wc-rns
) tool to test your required namespace communication. Just paste a walletconnect project id, and the uri from your dapp. wc-rns
will then display the required namespaces.
Required and Optional Chains
Required and Optional Methods
Batched transactions allow you to perform multiple transactions in one single on-chain transaction.
If you are building a dapp, you communicate the actions to the wallet that the user performs. Instead of sending a single transaction one after the other, you can send an array of transactions, and the smart wallet will execute the batch. This greatly improves the user experience and allows for one click interfaces.
Each wallet has one way or the other to execute batch transactions. Safe uses the multicall contract to batch, and other wallets might use different methods.
EIP-5792 allows us to standardise JSON-RPC methods for Dapps to communicate bundle calls to wallets using:
A simple example:
{
"jsonrpc": "2.0",
"id": 0,
"method": "wallet_sendFunctionCallBundle",
"params": [
{
"chainId": 1,
"from": "0xd46e8dd67c5d32be8058bb8eb970870f07244567",
"calls": [
{
"to": "0xd46e8dd67c5d32be8058bb8eb970870f07244567",
"gas": "0x76c0",
"value": "0x9184e72a",
"data": "0xd46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f072445675"
},
{
"to": "0xd46e8dd67c5d32be8058bb8eb970870f07244567",
"gas": "0xdefa",
"value": "0x182183",
"data": "0xfbadbaf01"
}
]
}
]
}
For more complete example: checkout this guide on how to leverage batch transactions for your dapp.
Sponsored transactions give your dapp the ability to pay for users' transaction fees.
Without sponsored transactions, anyone who sends an Ethereum transaction needs to have ETH to pay for gas fees. This forces new users to pass KYC and purchase ETH before they can start using your dapp. This can be a major hurdle for users without prior crypto experience that are unfamiliar with the concept of needing to keep ETH in their wallet for gas.
Thanks to smart wallets, your users can now interact with Ethereum smart contracts without needing ETH for transaction fees.
Example of sponsored transactions:
Partner with Paymasters
Many wallets rely on 3rd party paymaster services, or equivilant, for sponsored transactions. By partnering with a paymaster provider, you can enable sponsored transactions with all participating wallets.
Partner with Smart Wallets
You can partner directly with individual smart wallets based on your usecase to allow for sponsored transactions on your dapp.
Normally, when verifying signatures from regular accounts, which are Externally Owned Accounts (EOAs), you would use an ECDSA method called ecrecover() to retrieve the corresponding public key, which will then map to an address.
In the case of Smart Contract Wallets, you are not able to sign a message with the smart contract account.
Therefore, the recommended way is to check for smart contract wallet signatures is using ERC-6492, which extends ERC-1271. The reasons to use it:
eth_call()
to do all checks: either ecrecover, isValidSignature
on existing contract or isValidSignature
on pre-deployed contract.https://eips.ethereum.org/EIPS/eip-6492
You can verify whether a signature on behalf of a given counterfactual contract (even if not deployed yet) is valid. This standard extends ERC-1271.
The first argument is the address of the account, the second is the _hash
argument which accepts the hash of the message digest, and the third argument _signature
is the signed payload returned by the wallet upon signing.
import ethers from 'ethers';
import { verifyMessage } from '@ambire/signature-validator';
const provider = new ethers.providers.JsonRpcProvider('https://polygon-rpc.com')
async function run() {
// Replace `ethers.verifyMessage(message, signature) === signer` with this:
const isValidSig = await verifyMessage({
signer: '0xaC39b311DCEb2A4b2f5d8461c1cdaF756F4F7Ae9',
message: 'My funds are SAFU with Ambire Wallet',
signature: '0x9863d84f3119ac01d9e3bf9294e6c0c3572a07780fc7c49e8dc913806f4b1dbd4cc075462dc84422a9b981b2556f9c9197d76da7ba3603e53e9300869c574d821c',
// this is needed so that smart contract signatures can be verified
provider,
})
console.log('is the sig valid: ', isValidSig)
}
run().catch(e => console.error(e))
const isValidSignature = '0x01' === await provider.call({
data: ethers.utils.concat([
validateSigOffchainBytecode,
(new ethers.utils.AbiCoder()).encode(['address', 'bytes32', 'bytes'], [signer, hash, signature])
])
})
You can also take a reference from the following ethSign_6492.js test.
tx.origin
tx.origin
: This represents the original external user (i.e., an externally owned account or an externally deployed contract) that initiated the transaction. It is the very first sender of the transaction, even if the transaction was forwarded through multiple contracts. It's important to note that this can change if the transaction is forwarded through multiple contracts, and it can lead to unexpected behavior if used in certain situations.
tx.origin
breaks compatibility. Using it means that your contract cannot be used by another contract, because a contract can never be the tx.origin. It makes smart contract wallets incompatible with your contract if you assume it is equal to msg.sender
.
Don't assume that tx.origin
will be the one executing the transaction on chain. Smart Contract wallets commonly use relayer. ERC-4337 utilises a Bundlers network to sign and submit user operations onchains. Others might use a private relayer to do so.
Your dapp will receive the transaction hash in order to monitor the status of the transaction, and events will be emitted as usual. The "from" of this transaction will change, and that's execpted behaviour. Don't do like uniswap..
If you followed this guide, there's no real reason why you should need to detect whether the wallet connected is a smart wallet or not. However, if you really need: you can detect smart contract wallets by verifying on-chain if the exposed account address has any associated code deployed.
Using etherjs
import { providers, utils } from 'ethers'
const provider = new providers.JsonRpcProvider(rpcUrl)
const bytecode = await provider.getCode(address)
const isSmartContract = bytecode && utils.hexStripZeros(bytecode) !== '0x'
Using web3js
import Web3 from 'web3'
const web3 = new Web3(rpcUrl)
const bytecode = await web3.eth.getCode(address)
const isSmartContract = bytecode && utils.hexStripZeros(bytecode) !== '0x'
Using viem
import { createPublicClient, http } from 'viem'
import { mainnet } from 'viem/chains'
export const publicClient = createPublicClient({
chain: mainnet,
transport: http()
});
const bytecode = await publicClient.getBytecode({
address,
});
const isSmartContract = bytecode && utils.hexStripZeros(bytecode) !== '0x'
While this method effectively identifies smart contract wallets with deployed code, it's essential to be aware that this approach does not account for undeployed smart contract wallets