Passivum: Playing Around with Account Abstraction

Giovanni Zaarour
10 min readMay 24, 2023

Creating a social recovery DeFi dApp using the Biconomy SDK

After diving deeper into the topic of account abstraction, I developed a strong desire to build something on ERC-4337. When our developer team at Blockchain@USC was tasked with competing in a hackathon among other top universities, I immediately decided to lead us in the direction of AA. Since the hackathon was sponsored by EduDAO under BitDAO, we were tasked with building on the Mantle L2. I had recently seen that Mantle announced support for ERC-4337 via the Biconomy SDK, making this the perfect opportunity to make our own implementation of a smart contract wallet. Enter “Passivum Wallet” — a savings account for crypto.

What is Passivum?

Passivum is a smart contract wallet dApp built on top of the Biconomy SDK that allows users to stake their idle tokens in DEX liquidity pools for passive yields, all with just one click, and without paying any gas fee. Every account has social recovery features thanks to our custom social recovery module, which allows users to recover their wallet with the help of some friends’ addresses should they lose their private key. Additionally, users can entrust their wallet to some receiver address, unlocking ownership to the receiver after some designated amount of years, or a “dead man’s switch” for short. Gasless auto-staking in DEX pools with just one click is possible through ERC-4337 batch transactions and the paymaster, rather than users having to go through the ordeal of signing separately for an approval and then a deposit.

If some of these terms — such as social recovery, dead man’s switch, batch transactions, and paymaster — sound unfamiliar to you, I highly recommend first reading my in depth article on account abstraction where every intricacy of the topic is defined. You won’t understand the rest of this blog otherwise.

The whole point of account abstraction is to simplify and secure the crypto user experience. Sticking to this ethos, we wanted to make onboarding as simple as possible, which is why we used web3auth to prioritize easy onboarding. Users can create a new EOA and easily access it with their social medial or email login, without ever having to save a private key or seed phrase; the best part is that web3auth is still fully custodial. In a smart account, the EOA private key serves as the account owner and only designated transaction signer. However, all assets are stored within the smart contract — more on this later.

How it Works

Assuming you’re all caught up on the ERC-4337 architecture, we can dive straight into the Biconomy SDK. First, let’s begin with the smart contracts. In addition to the core ERC-4337 contracts, these are the notable pieces of on-chain code:

  • SmartAccount.sol — This is Biconomy’s actual implementation of the smart account. It is a singleton contract on chain, as it is deployed only once, and all the actual user smart accounts are deployed as proxy contracts which send delegatecalls to this singleton implementation. In layman’s terms, it acts as a blueprint for how the proxy smart accounts should act.
    — The implementation contains logic for handling transactions, including validateUserOp(), execTransaction(), _validateSignature(), and more.
    — The implementation also has constructor() and init() functions, which set the owner and EntryPoint addresses.
    — Another notable function is setOwner() which changes the private key allowed to sign transactions, and can only be called directly by the current owner or self-call.
    — SmartAccount.sol notably inherits ModuleManager.sol, BaseSmartAccount.sol, IAccount.sol, Executor.sol, and many others.
  • ModuleManager.sol — Allows a smart account to enable and disable modules, which are contracts that add additional logic to the smart account. Contains the important function execTransactionFromModule() which allows an approved module contract to initate a self-call to the smart account, since SmartAccount.sol inherits ModuleManager.sol. Essentially, a “approved” module can trigger the smart account to call one of its own functions. This is how our social recovery module is capable of calling the setOwner() function in smart account.
  • Proxy.sol — A basic proxy that delegates all calls to the singleton implementation, which is stored upon proxy deployment but can be changed by calling setImplementation().
  • SmartAccountFactory.sol — Deploys accounts as proxies and points them to the implementation contract. If the smart account address has been determined counterfactually — meaning the proxy has not yet been deployed but will be deployed at a pre-determined address upon sending the first transaction — then the factory will deploy the proxy with the CREATE2 opcode rather than CREATE, such that the order of creation of wallets doesn’t interfere with the generated addresses. Otherwise, if no counterfactual address generation has happened, the factory deploys proxies normally.
    — *If you watch the demo video at the end of this blog, you’ll see that the smart account address is determined as soon as a user connects their EOA. This address generation happens PRIOR to the actual smart account deployment, which truly happens upon the first transaction sent from it (whether that’s the social recovery setup, or something else). You can see this in the demo video, as the block explorer doesn’t recognize the smart account address until the first social recovery setup transaction is sent as a batch transaction.
  • SocialRecoveryModule.sol — This is our custom built, singleton module which stores a list of friend addresses and settings for the dead man’s switch and social recovery. There exist several mappings which assign the aforementioned social recovery settings to each smart contract wallet address. The setup() function initializes the list of friends and voting threshold for social recovery, and it must be called by the smart account. Similarly, the setDeadMansSwitch() function initializes the time of unlock and the designated receiver address, and must be called by the smart account. recoverAccess() and claimUnlock() are callable functions for social recovery and dead man’s switch recovery, respectively. More details are available in the code natspec.

Because each user smart account is a proxy contract, the singleton SmartAccount.sol that these proxies refer to can actually be replaced thanks to the updateImplementation() function within SmartAccount.sol. All this needs is a delegatecall directly from the proxy contract, which will point it to a brand new smart account implementation. This makes it possible to change the smart account logic should the user opt in to upgrade to a new version for whatever reason, such as vulnerabilities. Because they have initializer functions and changeable implementations, smart accounts are upgradeable smart contracts.

Transaction Flow

  • Gasless transaction: the user approves a signature from the dApp UI, sending it through the Biconomy SDK (in our frontend JS), which sends the UserOp data to the a Relayer (Biconomy’s alternative to Bundlers), going to the global ERC-4337 entry point contract, which checks if the transaction can be sponsored by the paymaster (the “gas tank” which the dApp must have filled with tokens), then finally sending the tx or bundle to the smart account (which has a function for handling transaction execution), ultimately going to the dApp smart contract.
  • In this case, the paymaster is Biconomy’s pre-deployed BiconomyVerifyingPaymaster, which we fill up with tokens for gas sponsorship, and then whitelist our SocialRecoveryModule address.
  • Batch Transaction: The graphic below illustrates an example of a user batching together a token approval and a token deposit into the Uniswap router. This is very similar to what we have implemented in our Passivum frontend. The upper flow chart shows the two separate user operations, and the bottom flow chart shows the overall batch transaction flow.
  • The main difference between the batch transaction flow and the previous gasless transaction flow is that the batch of transaction calldata is encoded as bytes and sent to the MultiSend.sol contract, which is a lib made by Biconomy. Even though it is not included in the graphic below, the batch still goes to the entry point and gets checked against the paymaster, if the batch is indeed gasless.

Paymaster Dashboard

Using the paymaster dashboard, we filled up the gas tank with some testnet MATIC and approved the social recovery module address, such that any new smart account can get their gas fee sponsored for transactions to this module. We also meant to add the UniswapV3 NonfungiblePositionManager contract, but completely forgot that part, whoops.

Backend and Relayer Service

According to the Biconomy documentation, the SDK relies on a backend client for tasks such as gas estimation (to make sure that the user or paymaster send enough with their transactions such that they don’t revert), indexer communication, and more. These APIs “perform tasks that are not effectively done on the client side.” A diagram is shown below:

As I last mentioned, Biconomy uses relayers instead of bundlers for forwarding UserOps to the entry point. Judging from the left side of their paymaster dashboard, I believe their goal is to change this in the future and allow node operators to create bundlers for Biconomy. However, as of right now, relayer nodes do the operations of bundlers such as signing valid transactions, paying their gas fees, and sending them to the network. Here is a description of relayers, from the Biconomy docs:

Each Relayer is essentially an EOA. Biconomy’s relayer infrastructure composes of multiple such EOAs on each chain. Every relayer has an inbuilt auto-scaling functionality. Thus in cases of a high load of transactions, the relayer infra can auto-spin up additional relayers to handle this.

The Relayer Node Service is divided into the following main components.

Common: Has all the services that would be shared by every entity of the project. These include database, caching, gas price service, network service, logger, etc.

Server: Has all the routes along with validations of request bodies and simulation check middleware.

Relayer: Contains the core logic of relayer management and transaction handling. This part also takes care of transaction resubmission and notifying the client regarding the state of a transaction.

And a somewhat confusing diagram of the relayer node service:

Much more technical explanation is available on the Biconomy docs site.

dApp Frontend

For the Passivum dApp, we implemented a simple home page which allows a user connect their EOA wallet using web3auth and view their EOA address and smart account address. The user can enter one of two pages — a page to set up their social recovery settings, and another to manage their assets.

  • Social Recovery Page: the user goes through two steps: one to add friends and another to set the dead mans switch receiver. After submitting a gasless signature, the following 3–4 transactions get batched and sent:
    IF the smart account has not been deployed as proxy yet, the relayer will call SmartAccountFactory.sol to deploy the proxy to its counterfactual address using CREATE2 opcode
    — Call setup(address[] memory _friends, uint256 _threshold) to the social recover module
    — Call setDeadMansSwitch (bool _on, address _receiver, uint256 _unlockTime) to the social recovery module
    — Call enableModule(address module) to the smart account (which inherits module manager), passing it the address of the social recovery module
  • Manage Assets Page: this page allows the user to view the assets deposited into their smart account, and also has a form to deposit more funds from their EOA into the smart account, simply calling the transfer() function from any ERC20 token to the smart account address. Most importantly, the user can access the stake form to send a batch transaction to mint a new liquidity pool position in Uniswap V3, for a token pair of their choice. After submitting a gasless signature, the following 3–4 transactions get batched and sent:
    IF the smart account has not been deployed as proxy yet, the relayer will call SmartAccountFactory.sol to deploy the proxy to its counterfactual address using CREATE2 opcode
    — Call approve(address spender, uint256 value) on the first token’s contract to allow UniswapV3 NonFungiblePositionManager access to the smart account’s tokens
    — Call approve(address spender, uint256 value) on the second token’s contract
    — Call mint() to the NonFungiblePositionManager with a set of parameters we defined. Note that, in a production version of this dApp, more optimal checks and values for slippage protection would be included in these parameters

Watch the Demo

trust me, you’ll enjoy it :). (excuse my constant throat-clearing)

It’s All on Github

https://github.com/BlockchainUSC/passivum_wallet

Special thanks to the rest of our Blockchain@USC engineering team — Matthew Salaway, Aaron Ly, Logan Norman, and Felix Chen.

--

--