Building Account Abstraction ERC-4337 (Part-3) Paymaster

Shishir Singh
cumberlandlabs
Published in
5 min readJul 23, 2023
Jim Corbett National Park Uttarakhand

In Part-1 and Part-2 of this series, we built an on-chain wallet contract with Execute method and transferred a few WETH tokens from the Alice wallet contract to Bob. However, the Alice wallet contract pays the gas for this transaction. If User Alice wants to take the sponsorship from a 3rd party to pay gas on her behalf, we have to leverage the Paymaster contract.

In this article, we will first set up our paymaster contract, fund the contract with enough ETH to pay for gas, and then use this paymaster.

Refer to the complete code here.

Deploy Paymaster

I have used VerifyingPaymaster.sol for our deployment. Below is one such deployment script to deploy the paymaster contract. You can choose your favorite framework for deployment.

const testData =  require('../testData.json');
const entryPoint = require('../EntryPoint.json');
var C2 = artifacts.require ("./samples/VerifyingPaymaster.sol");
var C3 = artifacts.require ("./samples/DepositPaymaster.sol");
const fs = require('fs');
module.exports = function(deployer) {
deployer.then(async () => {

await deployer.deploy(C2,entryPoint.address,testData.coordinatorPublicKey);
await deployer.deploy(C3,entryPoint.address);

var json = JSON.stringify({
verifyingPaymasterAddress: C2.address,
depositPaymasterAddress: C3.address });

fs.writeFileSync('deployData.json', json);
console.log(`Deployment Done !!`)
});
}

Capture the result of the deployment in a JSON file. The address could be different for you. Even though I have deployed two contracts, VerifyingPaymaster, and another DepositPaymaster, for this demo, I have used the VerifyingPaymaster, not the latter one, primarily since I am using ETH as the base token for Gas payment sponsorship and not any other ERC20 token.

However, in future articles, if time permits, I will demonstrate how to pay for the gas with any ERC20 token. All it takes is to deposit enough ERC20 tokens into DepositPaymaster contract, swap them with equivalent ETH using Oracle Contract, and pay gas with ETH.

{
"verifyingPaymasterAddress": "0x0a7e36e14fdC6787A6668d057DBAE718bBFdA8f6",
"depositPaymasterAddress": "0x8015c97Fc294919d38c00A2BE6f722ca3f38ed98"
}

Execute via Paymaster

Let’s get back to the task; once our paymaster contract is deployed successfully, we will execute the call data similar to Part-2 but with a significant twist. This time gas for the transaction is paid via Paymaster, not via the user's on-chain wallet.

To provision, we will change our init() function code and pass the viaPaymaster flag to true.

async function init() {

await initAddresses() ;

// await composeInitCode(); // Already executed in Part-1

// await fundContractsAndAddresses() ; // Already executed in Part-1

// await composeWETHTransferCallData(); // Already executed in Part-1

// await executeHandleOps(initCode,'0x', false) ; // Already executed in Part-1

// await executeHandleOps(initCode,'0x', true) ;

// await executeHandleOps('0x',callData, false) ; // Already executed in Part-1

await getBalance() ; // pre transfer balances console

await composeWETHTransferCallData();

await executeHandleOps('0x',callData, true) ;

await getBalance() ; // post transfer balances console

}

Inside executeHandleOps() function if the viaPaymaster = true, it will compose the Paymaster and Its related data as below.

The signer for the paymaster contract will always be the its deployer address private key. In our case its the cooordinator private key

async function composePaymasterAndData(ops){

ops.paymasterAndData = hexConcat([payMasterAddress, defaultAbiCoder.encode(['uint48', 'uint48'], [MOCK_VALID_UNTIL, MOCK_VALID_AFTER]), '0x' + '00'.repeat(65)])
ops.signature = '0x' ;
const hash = await payMasterContract.methods.getHash(ops, MOCK_VALID_UNTIL, MOCK_VALID_AFTER).call() ;
const signer = new ethers.Wallet(coordinatorPrivateKey, provider);
const sign = await signer.signMessage(arrayify(hash));
const paymasterAndData = hexConcat([payMasterAddress, defaultAbiCoder.encode(['uint48', 'uint48'], [MOCK_VALID_UNTIL,MOCK_VALID_AFTER]), sign])
return paymasterAndData ;
}

Run the code and examine the pre- and post-ETH, WETH, and Paymaster ETH balances.

Alice ETH Balance 4790.998004326175016756
Alice sender wallet 0x0131656c221e4F779f270045d3B2dEeF70219A39 ETH Balance 1.979220694827756292
Paymaster ETH Balance 3.999858265414895781
Alice WETH Balance 0.5
Alice sender wallet 0x0131656c221e4F779f270045d3B2dEeF70219A39 WETH Balance 0.23
Bob WETH Balance 0.02
Handleops Success --> 0x21508219ac09ce37818411cf1ce7838aec705fb10d39e4a3b5ff855fd80f6be2
Alice ETH Balance 4790.998004326175016756
Alice sender wallet 0x0131656c221e4F779f270045d3B2dEeF70219A39 ETH Balance 1.979220694827756292
Paymaster ETH Balance 3.999747636726359289

Alice WETH Balance 0.5
Alice sender wallet 0x0131656c221e4F779f270045d3B2dEeF70219A39 WETH Balance 0.22
Bob WETH Balance 0.03

We can see in the above console paymaster ETH balance was 3.999858265414895781, and post-transfer, it got changed to 3.999747636726359289. And the Alice sender wallet ETH balance remains the same as 1.979220694827756292.

Below is the sequence diagram of what we have just executed above in the code:

Account Abstraction Gas Payment via Paymaster contract

Wallet Creation via Paymaster

In this section, we will try to leverage our Paymaster paying gas even for the on-chain wallet creation for the user. Since we have already used the salt as 0 in Part-1 to create the wallet, we will use any other unique value (salt as 2) to create the on-chain wallet for Alice.

async function init() {

await initAddresses() ; // Already executed in Part-1 for Salt 0 now we are using Salt as 2

await composeInitCode(); // Already executed in Part-1 for Salt 0 now we are using Salt as 2

await fundContractsAndAddresses() ; // Fund new wallet address with Salt as 2

await composeWETHTransferCallData(); // Similar as in Part-2

// await executeHandleOps(initCode,'0x', false) ;

await getBalance() ;

await executeHandleOps(initCode,'0x', true) ;

// await executeHandleOps('0x',callData, false) ;

// await composeWETHTransferCallData();

// await getBalance() ;

// await executeHandleOps('0x',callData, true) ;

await getBalance() ;

}

Run the code and examine the pre- and post-ETH, WETH, and Paymaster ETH balances.

Alice ETH Balance 5786.497616850782249254
Alice sender wallet 0xe4F6777Bd3f7B2665Df76B07D1d3ffd849Aa7e9e ETH Balance 2
Paymaster ETH Balance 5.999747636726359289
Alice WETH Balance 0.75
Alice sender wallet 0xe4F6777Bd3f7B2665Df76B07D1d3ffd849Aa7e9e WETH Balance 0.25
Bob WETH Balance 0.03
Handleops Success --> 0x81c8cd4c579fadd0bec21396e955535182128d5713e3cc652203e84f3e482323
Alice ETH Balance 5786.497616850782249254
Alice sender wallet 0xe4F6777Bd3f7B2665Df76B07D1d3ffd849Aa7e9e ETH Balance 2
Paymaster ETH Balance 5.999561782288280171
Alice WETH Balance 0.75
Alice sender wallet 0xe4F6777Bd3f7B2665Df76B07D1d3ffd849Aa7e9e WETH Balance 0.25
Bob WETH Balance 0.03

We can see in the above console paymaster ETH balance was 5.999747636726359289, and post-transfer, it got changed to 5.999561782288280171. And the new Alice sender wallet has changed to 0xe4F6777Bd3f7B2665Df76B07D1d3ffd849Aa7e9e (with Salst as 2), and its ETH balance remains the same as 2.

Future Business Opportunities

Paymaster-as-a-service: Entities and Corporates can establish themself as Paymasters and provide various services to users to pay their Gas on their behalf. As a business model, they could charge a simple fiat off-chain, ETH on-chain, or any other ERC20 token and pay gas on the user’s behalf.

With this service in place, users need not worry about buying/swapping their ETH/Token20. User can simply pay their gas upfront via any preferred payment channel and use these services like a gas sponsorship provider to pay their on-chain transactions gas.

Next Steps

We have seen all the 3 major operations for ERC-4337 account abstraction. I will leverage the Deposit Paymaster contract with an ERC20 token in future articles in this series.

--

--