Account Recovery

Web2 Recovery

User Story

The user will create their wallet with a specific recovery method provided, such as a google account.

When the recovery sequence is initiated, the user will send a OpenID Connect JWT token they fetch from their google account, generate a zk proof showing ownership of the token which is then verified on chain.

If the verification is successful, the account ownership will be transferred to a new address after a period of seven days, during which the original owners can submit a transaction to cancel the ownership transfer.

How it works

  1. The user adds their web2 recovery method to their funwallet, for example, they can add their google account albert@fun.xyz
  2. When a user initiates the recovery process, they go to their identity provider - in this case google - and fetch and sign their OpenID connect JWT token.
  3. The user then generates a Zk proof via their clients or a server fun.xyz will be running
  4. The user sends the recovery userOp and the proof to our zk aggregator, which takes proofs and aggregates them together to reduce gas costs
  5. The zk aggregator then sends the aggregated proof back to the user
  6. The user sends the recovery userOp to the entryPoint via the bundler, just like a normal EIP-4337 transaction.
  7. The onchain verifier validates the proof and ensures that the web2 authentication method was provided correctly
  8. The verifier sends a success/fail state back to the entrypoint.
  9. The ownership transfer stage is set in the funwallet.
  10. The ownership of the account is transferred to the new owner after a time period of seven days, during which the original owners can submit a transaction to cancel the ownership transfer.

Please note that all of these steps will be abstracted away via the funkit core and funkit react SDKs.

Code Snippets

Setup

const API_KEY = "LEJPI01a9nE7vRwk3dct6QbtSC3TJEE8A9KKiYR9";
const FUNKIT_OPTIONS: FunKitOptions = {
  chain: ChainId.Goerli,
  apiKey: API_KEY,
  gasStrategy: GasStrategy.GoerliGassless,
};

const WALLET_INDEX = 0;

function setUpAccountRecovery(socialOauthProvider: SocialOauthProvider) {
    const { funWallet } = useSocialWallet(
        FUNKIT_OPTIONS,
        socialOauthProvider,
        WALLET_INDEX
    )
    await funWallet.setUpAccountRecovery({ 
        type: Recovery.web2,
        provider: Web2.Google,
        identifier: "albert@fun.xyz"
    })
}

Recovery

import {OAuth2} from "@google/auth"

const API_KEY = "LEJPI01a9nE7vRwk3dct6QbtSC3TJEE8A9KKiYR9";
const FUNKIT_OPTIONS: FunKitOptions = {
  chain: ChainId.Goerli,
  apiKey: API_KEY,
  gasStrategy: GasStrategy.GoerliGassless,
};

const WALLET_INDEX = 0;

function setUpAccountRecovery(socialOauthProvider: SocialOauthProvider) {
    const { funWallet } = useSocialWallet(
        FUNKIT_OPTIONS,
        socialOauthProvider,
        WALLET_INDEX
    )
    await funWallet.recover({ 
        type: Recovery.web2,
        provider: Web2.Google,
        identifier: "albert@fun.xyz",
        token: await OAuth2.getJWT()
    })
}

Security

The zero knowledge proof on the OpenId Connect JWT token solves the biggest problem with JWT style verifications. By submitting a proof and not the actual JWT token onchain, we make it much more difficult for a third party to perform a replay attack. Additionally, we will have an expiration timestamp on the proof to prevent replay attacks.

Social Recovery

User Story

The user will create their wallet with a group of N signers.

The recovery sequence can be initiated when M of N signers sign the same data and send this data in a userOp.

If the verification is successful, the account ownership will be transferred to a new address after a period of seven days, during which the original owners can submit a transaction to cancel the ownership transfer.

How it works

  1. The user adds their web2 recovery method to their funwallet and chooses the signers for the wallet
  2. M of the N signers must sign the recovery data and send the hash in a userOp to the EntryPoint
  3. The userop is executed and ownership of the account is transferred to the new owner after a time period of seven days, during which the original owners can submit a transaction to cancel the ownership transfer.

Please note that all of these steps will be abstracted away via the funkit core and funkit react SDKs.

Code Snippets

Setup

const API_KEY = "LEJPI01a9nE7vRwk3dct6QbtSC3TJEE8A9KKiYR9";
const FUNKIT_OPTIONS: FunKitOptions = {
  chain: ChainId.Goerli,
  apiKey: API_KEY,
  gasStrategy: GasStrategy.GoerliGassless,
};

const WALLET_INDEX = 0;

function setUpAccountRecovery(socialOauthProvider: SocialOauthProvider) {
    const { funWallet } = useSocialWallet(
        FUNKIT_OPTIONS,
        socialOauthProvider,
        WALLET_INDEX
    )
    await funWallet.setUpAccountRecovery({ 
        type: Recovery.social,
        users: ["vitalik.eth", "funxyz.eth", "0x25E07d90a4e2e9fb627FcF5f4FAd5165dDA6d592"],
        threshold: 2
    })
}

Recovery

import {ethers} from 'ethers';
const API_KEY = "LEJPI01a9nE7vRwk3dct6QbtSC3TJEE8A9KKiYR9";
const FUNKIT_OPTIONS: FunKitOptions = {
  chain: ChainId.Goerli,
  apiKey: API_KEY,
  gasStrategy: GasStrategy.GoerliGassless,
};
const provider = new ethers.providers.JsonRpcProvider("infura.io");
const WALLET_INDEX = 0;

function setUpAccountRecovery(socialOauthProvider: SocialOauthProvider) {
    const { funWallet } = useSocialWallet(
        FUNKIT_OPTIONS,
        socialOauthProvider,
        WALLET_INDEX
    )
    
    const vitalik = new ethers.Wallet("0x123..123", provider);
    const funxyz = new ethers.Wallet("0x123..123", provider);
    const recoverySignatureData = await funwallet.getRecoveryHash();
    
    const signature1 = await vitalik.signMessage(recoverySignatureData);
    const signature2 = await funxyz.signMessage(recoverySignatureData);
    
    await funWallet.recover({
        type: Recovery.social,
        recoverySignatures: [signature1, signature2]
    })
}

Security

This idea is heavily based on Vitalik's vision for social recovery wallets: https://vitalik.ca/general/2021/01/11/recovery.html

The largest risk to this approach is a lack of availability for the current wallet owner. If the recovery signers collude, they could take ownership of the funwallet. To prevent this, we will allow funwallets to have arbitrarily long veto periods, wherein a transaction from the current wallet owner can stop the ownership transfer.

Social Recovery Protocol Changes

configuration = single userOp: superuser creates recovery group in UserAuth and configures the group in RBAC to allow it to call UserAuth.recover() and nothing else.

  • I'm assuming in this case superuser refers to an owner, since no one else at this point has the ability to create a new group in UserAuth and configure RBAC?

initiate recovery = single userOp: third-party calls initiateRecovery on UserAuth, providing group information for the new, receiving group, which after recovery will be the new superuser groupx

  • Not sure how you envision this to be implemented, but if any third party can call this function(since in thiss flow the validation for recover doesn't happen in initiateRecovery()), someone could just repeatedly call this function. Wouldn't it make more sense to make it such that only the recovery group can call initiateRecovery()?

(timelock period)

confirm recovery = single userOp: recovery group calls UserAuth.recover() which overwrites the wallet’s superuser group (without changing the group ID).

  • What is the point of overwriting the wallet's superuser group? By superuser, do you mean owner?

  • If superuser != owner, what permissions does it have and when is the superuser group configured?