Home

Awesome

Governance Paymasters

This work was funded via a grant from the Ethereum Foundation. 🙏

This repository contains Paymasters that operate fully on-chain, as in without requiring a separate centralized backend service that determines whether a transaction should be covered by a Paymaster.

Why is this interesting?

Most paymasters today (like VerifyingPaymaster) require a backend service. They typically only verify whether UserOp.paymasterAndData contains a valid signature and relies on an external service to parse and validate transaction. While this is functional, it introduces a centralized service in the middle.

In this repository, we demonstrate several paymasters that don't need a centralized service. These paymasters work for only a specific action on-chain and they determine whether to pay for those actions with logic completely on-chain. Once deployed, they can operate without requiring any intervention (except to refill their accounts with EntryPoint).

Table of Contents

General Overview

Methodology

Main challenge for a fully on-chain paymaster is to cover only the transactions specified and avoid getting drained through various different abuse vectors.

Our validation function checks for both the length and subsections of the calldata to ensure the call will only go to the intended contract and only call a specific function (like delegate(address)) on that contract.

In each of the paymasters below, we specify the exact calldata required for the paymaster to approve.

Other Considerations: attack vectors, gas costs, etc

Storage access rules

One of the reasons on-chain Paymasters are challenging to build is due to strict storage access rules that prevent attacks.

These paymaster respects all the storage access rules. They only access ERC20 Token balances, which is allowed by the rule #3 in the specifications. Additionally, the Paymaster accesses its own storage and that requires it to stake with EntryPoint (which our deploy script handles).

Update: Per conversation with the AA team at EF, these rules have been relaxed in the latest version. Now, Paymasters can access any storage as long as they stake with EntryPoint. Main risk to mitigate: accessing highly volatile storage can be used against a paymaster. It can create situations where a userop can become invalid between validation and execution.

Governor Support

We initially built these paymasters for GovernorBravo and later tested them successfully for OpenZeppelin's Governor contract with minimal modifications and now they support both types of Governors.

One caveat is that our implementations only support delegate(address) and castVote(uint256,uint8) function calls for the OZ Governor. Unlike GovernorBravo, Governor also allows casting votes through additional calls like castVoteWithReason and castVoteWithReasonAndParams. While the paymaster can be extended to support those calls, we believe those can be used to drain the paymasters faster. So, implementing them requires additional checks like limiting length of reason or params.

<details> <summary> Testing with `GovernorBravo`</summary>

Check out scripts 05_DeployGovernorBravo... to 08_GBCastVote.s.sol. These scripts will

You'll need to update some the variables in .env (scripts will let you know).

</details> <details> <br> <summary> Testing with OpenZeppelin's `Governor`</summary>

Check out scripts 09_DeployOZGovernor... to 12_OZBCastVote.s.sol. These scripts will

You'll need to update some the variables in .env (scripts will let you know).

</details>

You'll need access to a bundler like Stackup to submit a userop with paymasterAndData.

Other areas to explore

1. Paymaster for delegate(address) call

This paymaster covers the gas cost of delegate(address) function used by ERC20 tokens. This function usually needs to be called before an owner can vote in their respective DAO. For example, Uniswap DAO is managed by UNI token holders. Those token holders can either delegate to themselves or delegate to another wallet address to vote on their behalf. That delegate function call could be paid for by this Paymaster.

Code: PaymasterDelegateERC20.sol

Calldata

Sample calldata required by this Paymaster:

    /**
     * Ex: ERC20 Delegate call. Total 196 bytes
     *  0x
     *  b61d27f6 "execute" hash
     *  0000000000000000000000001f9840a85d5af5bf1d1762f925bdaddc4201f984 ex: ERC20 address
     *  0000000000000000000000000000000000000000000000000000000000000000 value
     *  0000000000000000000000000000000000000000000000000000000000000060 data1
     *  0000000000000000000000000000000000000000000000000000000000000024 data2
     *  5c19a95c "delegate" hash
     *  000000000000000000000000b6c7ff166b0d27aa6132673838995f0fa68c7676 delegatee
     *  00000000000000000000000000000000000000000000000000000000 filler
     */

Other Considerations

Example Transactions & Gas Usage

Deployed at: 0x5faEe2339C65944935DeFd85492948ea6079c745

We compare calling the delegate(address) function from various different wallets.

WalletPaymasterSample TxnGas Used
EOA-Txn95,737
AA - already deployedNoneTxn167,689
AA - not deployedNoneTxn452,501
AA - already deployedPaymasterDelegateERC20Txn187,815
AA - not deployedPaymasterDelegateERC20Txn472,650

As you can see, gas usage of AA wallet vs EOA is quite high but the Paymaster itself is a pretty minimal increase in gas usage.

2. Paymaster for castVote(uint256,uint8) call

This Paymaster only pays for the castVote call. This call is typically used by on-chain governance systems to record a vote.

Code: PaymasterCastVote.sol

Calldata

Sample calldata required by this Paymaster:

    /**
     * Ex: GovernorBravo castVote. Total 228 bytes
     * 0x
     * b61d27f6 "execute" hash
     * 000000000000000000000000408ed6354d4973f66138c91495f2f2fcbd8724c3 "governorBravoAddress"
     * 0000000000000000000000000000000000000000000000000000000000000000 value
     * 0000000000000000000000000000000000000000000000000000000000000060 data1
     * 0000000000000000000000000000000000000000000000000000000000000044 data2
     * 56781388 "castVote" hash
     * 0000000000000000000000000000000000000000000000000000000000000034 proposalId
     * 0000000000000000000000000000000000000000000000000000000000000001 support
     * 00000000000000000000000000000000000000000000000000000000 filler
     */

Other considerations

Example Transactions & Gas Usage

Deployed at: 0x6d1915457789DdA5A0f32D006edC7Bf0cdB3f746.

We compare calling the castVote(uint256,uint8) function from various different wallets.

WalletPaymasterSample TxnGas Used
EOA-Txn76,042
AA - already deployedNoneTxn162,656[!!]
AA - already deployedPaymasterCastVoteTxn160,655
AA - not deployedPaymasterCastVoteTxn472,650

[!!]: Counterintuitive that a transaction without Paymaster is more gas. We believe this is due to an extra transfer of ETH when no Paymaster is used.

3. Paymaster for delegate(...) or castVote(...) calls

This paymaster combines functionality of the above two paymasters into a single contract. It can support either delegate(...) or castVote(...) call.

Code: PaymasterDelegateAndCastVote.sol

Calldata

Same as the calldatas mentioned above. It can accept either of them - branching based on the length. 196 bytes for a delegate call and 228 bytes for a castVote call.

Other considerations

Example Transactions & Gas Usage

Deployed at: 0x2cEa8A3135A1eF6E5Dc42E094f043a9Bc4D27bC5.

WalletPaymasterGovernorSample TxnGas Used
AA - already deployedPaymasterDelegateAndCastVoteGovernorBravoTxn161,482
AA - already deployedPaymasterDelegateAndCastVoteOZGovernorTxn149,262

As expected, gas usage of this paymaster to be similar to the above two paymasters. OZGovernor is likely more gas optimized than GovernorBravo.

Usage

Build

$ forge build

Test and Coverage

Check out tests/. We have 100% test coverage of all the Paymasters in this repo.

$ forge test
$ forge coverage --report summary

Deploy

Copy .env.example to .env and update all the variables with your details: PRIVATE_KEY, PUBLIC_KEY, ETHERSCAN_API_KEY and ${chainName}_RPC_URL.

This will also deposit and stake ETH with Entrypoint. Ensure that you have enough ETH in the account listed in .env.

$ source .env
$ forge script DeployAndSetupScript --rpc-url $SEPOLIA_RPC_URL --private-key $PRIVATE_KEY --verify -vv --skip test --broadcast

Abandon

When you are done with a paymaster, it's useful to withdraw the remaining ETH from Entrypoint.

Update PAYMASTER variable with the deployed paymaster's address in .env. Then, run:

$ forge script AbandonScript --rpc-url $SEPOLIA_RPC_URL --private-key $PRIVATE_KEY --skip test --broadcast 

Questions/Comments

You are welcome to open Issues for any comments or reach us on Twitter/X.