Merge branch 'main' of github.com:getwax/wax into wax-215-add-compression

This commit is contained in:
jacque006
2024-06-18 16:59:11 -04:00
142 changed files with 11430 additions and 1317 deletions

View File

@@ -0,0 +1,62 @@
# Based on https://dev.to/daslaw/deploying-a-vite-app-to-github-pages-using-github-actions-a-step-by-step-guide-2p4h
name: packages/demos/email-recovery
on:
push:
branches: ['main']
paths:
- packages/demos/email-recovery/**
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
defaults:
run:
working-directory: ./packages/demos/email-recovery
# Sets the GITHUB_TOKEN permissions to allow deployment to GitHub Pages
permissions:
contents: read
pages: write
id-token: write
# Allow one concurrent deployment
concurrency:
group: 'pages'
cancel-in-progress: true
jobs:
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up Node
uses: actions/setup-node@v3
with:
node-version: 18
cache: 'yarn'
cache-dependency-path: packages/demos/email-recovery/yarn.lock
- name: Install dependencies
run: yarn install --frozen-lockfile
- name: Copy .env.base-sepolia
run: cp .env.base-sepolia .env
- name: Build
env:
VITE_WALLET_CONNECT_PROJECT_ID: ${{ secrets.VITE_WALLET_CONNECT_PROJECT_ID }}
run: VITE_WALLET_CONNECT_PROJECT_ID=${VITE_WALLET_CONNECT_PROJECT_ID} yarn build
- name: Setup Pages
uses: actions/configure-pages@v3
- name: Upload artifact
uses: actions/upload-pages-artifact@v1
with:
path: './packages/demos/email-recovery/dist'
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v1

View File

@@ -57,9 +57,10 @@ jobs:
forge build --sizes
id: build
# Skip safe zk email recovery unit tests while finishing demo. We still have a passing integration test - SafeZkEmailRecoveryPluginIntegration.t.sol
- name: Run Forge tests
run: |
forge test -vvv
forge test --no-match-path test/unit/safe/SafeZkEmailRecoveryPlugin.t.sol -vvv
id: test
hardhat:
@@ -96,5 +97,8 @@ jobs:
- name: Install Yarn dependencies
run: yarn install --frozen-lockfile
- name: Copy env file
run: cp .env.example .env
- name: Run hardhat compile
run: yarn hardhat compile

17
.gitmodules vendored
View File

@@ -9,7 +9,7 @@
url = https://github.com/foundry-rs/forge-std
[submodule "packages/plugins/lib/openzeppelin-contracts"]
path = packages/plugins/lib/openzeppelin-contracts
url = https://github.com/OpenZeppelin/openzeppelin-contracts
url = https://github.com/openzeppelin/openzeppelin-contracts
[submodule "packages/plugins/lib/safe-contracts"]
path = packages/plugins/lib/safe-contracts
url = https://github.com/safe-global/safe-contracts
@@ -28,3 +28,18 @@
[submodule "packages/plugins/lib/kernel"]
path = packages/plugins/lib/kernel
url = https://github.com/zerodevapp/kernel
[submodule "packages/plugins/lib/erc7579-implementation"]
path = packages/plugins/lib/erc7579-implementation
url = https://github.com/erc7579/erc7579-implementation
[submodule "packages/plugins/lib/reference-implementation"]
path = packages/plugins/lib/reference-implementation
url = https://github.com/erc6900/reference-implementation
[submodule "packages/plugins/lib/ether-email-auth"]
path = packages/plugins/lib/ether-email-auth
url = https://github.com/zkemail/ether-email-auth
[submodule "packages/plugins/lib/openzeppelin-contracts-upgradeable"]
path = packages/plugins/lib/openzeppelin-contracts-upgradeable
url = https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable
[submodule "packages/plugins/lib/zk-email-verify"]
path = packages/plugins/lib/zk-email-verify
url = https://github.com/zkemail/zk-email-verify

View File

@@ -0,0 +1,2 @@
VITE_WALLET_CONNECT_PROJECT_ID=REDACTED
VITE_RELAYER_URL=https://auth.prove.email/

View File

@@ -0,0 +1,2 @@
VITE_WALLET_CONNECT_PROJECT_ID=YOUR_PROJECT_ID
VITE_RELAYER_URL=https://auth.prove.email/

View File

@@ -0,0 +1,18 @@
module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react-hooks/recommended',
],
ignorePatterns: ['dist', '.eslintrc.cjs'],
parser: '@typescript-eslint/parser',
plugins: ['react-refresh'],
rules: {
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
}

View File

@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@@ -0,0 +1,42 @@
# Email Recovery Demo
Based on `yarn creat vite w/ React, Typescript`
## Deps
- NodeJS
- yarn
## Setup
```sh
yarn
yarn setup # this will overwrite your existing .env file
```
You will need to set `VITE_WALLET_CONNECT_PROJECT_ID` . You can create a new WalletConnect project at https://cloud.walletconnect.com/
## Run
```sh
yarn dev
```
## Base Sepolia Guide
### Connecting your Safe
1. Start the app locally by following the setup instructions above, or visit https://getwax.github.io/wax. If running locally, remember to generate the WalletConnect project ID.
2. Ensure you have a Safe account deployed to Base Sepolia. This is easiest to do through the Safe Wallet UI at https://app.safe.global. Connect your signer(s) e.g. MetaMask
3. Click the "Connect Wallet" button, choose the WalletConnect option, and then "Copy to Clipboard". This copies a pairing code that can be used to connect your Safe to the recovery dApp.
4. Return to the Safe Wallet UI and look for the WalletConnect icon, it's located next to your connected account info at the top right of the screen on desktop. Click on the icon and paste the pairing code - it should connect automatically and you should see a ZKEmail icon alongside the WalletConnect icon in the UI.
### Enabling the recovery module
5. In the recovery dApp, click "Enable Email Recovery Module", you should then be prompted in the Safe UI to confirm this transaction.
### Configuring the recovery module and adding a guardian
6. Now the recovery module has been enabled, you can configure recovery and request a guardian. Enter the guardians email address and also the recovery delay in seconds (so for a 10 second delay, enter the number 10). Then click "Configure Recovery & Request Guardian" and confirm the transaction in your Safe. This will add the required recovery config to the recovery module. The relayer will also be called under the hood and will send an email to your guardian so that they can confirm they agree to be your guardian. This additional confirmation from the guardian helps to prevent mistakes when adding the guardian to the recovery config. The recovery delay is a security feature that adds a delay from when recovery is approved until recovery can actually be executed. This protects against malicious recovery attempts where a guardian or hacker tries to take over an account - when this happens, the account owner can cancel the recovery while the delay is in progress.
7. Your guardian should now receive an email asking them to confirm this request by replying "Confirm" to the email. After about a minute or two of the guardian confirming, they should get a confirmation that they have been accepted as a guardian successfully. Under the hood, the relayer is generating the zkp from the email and verifying it onchain. Your recovery module is now setup and ready to go!
### Recovering your Safe
8. To initiate the recovery process, paste your new owner address into the "New Owner" field and click "Request Recovery".
9. Your guardian will receive an email asking them to confirm the recovery request. They can do this by replying "Confirm" to the email. The relayer will then generate a zkp from this email and verify it onchain. After about a minute or two, the guardian will receive an email confirmation that their recovery approval has been a success.
10. After the recovery delay has passed, click the "Complete Recovery" button in the recovery dApp. This will rotate the owner on the Safe and replace it with the new owner. Refresh the Safe Wallet app and visit settings to see the new owner rotated successfully.

View File

@@ -0,0 +1,7 @@
{
"verifier": "0xEdC642bbaD91E21cCE6cd436Fdc6F040FD0fF998",
"dkimRegistry": "0xC83256CCf7B94d310e49edA05077899ca036eb78",
"emailAuthImpl": "0x1C76Aa365c17B40c7E944DcCdE4dC6e6D2A7b748",
"simpleWalletImpl": "0xabAA8B42d053a57DeC990906ebdF3efF6844A861",
"safeZkSafeZkEmailRecoveryPlugin": "0xFcfE6030952326c90fc615DDD15a3945f62AfCef"
}

View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/png" href="https://i.imgur.com/46VRTCF.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Safe Email Recovery Demo</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@@ -0,0 +1,40 @@
{
"name": "email-recovery",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"setup": "cp .env.base-sepolia .env",
"dev": "vite",
"build": "vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
},
"dependencies": {
"@tanstack/react-query": "^5.28.14",
"@wagmi/cli": "^2.1.4",
"axios": "^1.6.8",
"circomlibjs": "^0.1.7",
"connectkit": "^1.7.3",
"ethers": "^6.11.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"viem": "2.x",
"wagmi": "^2.5.18"
},
"devDependencies": {
"@types/axios": "^0.14.0",
"@types/circomlibjs": "^0.1.6",
"@types/react": "^18.2.66",
"@types/react-dom": "^18.2.22",
"@typescript-eslint/eslint-plugin": "^7.2.0",
"@typescript-eslint/parser": "^7.2.0",
"@vitejs/plugin-react": "^4.2.1",
"eslint": "^8.57.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.6",
"typescript": "^5.2.2",
"vite": "^5.2.0",
"vite-plugin-node-polyfills": "^0.21.0"
}
}

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,31 @@
#root {
}
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.react:hover {
filter: drop-shadow(0 0 2em #61dafbaa);
}
@keyframes logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@media (prefers-reduced-motion: no-preference) {
a:nth-of-type(2) .logo {
animation: logo-spin infinite 20s linear;
}
}

View File

@@ -0,0 +1,56 @@
import { createContext, useEffect, useState } from "react";
import "./App.css";
import ConnectWallets from "./components/ConnectWallets";
import Navbar from "./components/Navbar";
import RequestedRecoveries from "./components/RequestedRecoveries";
import RequestGuardian from "./components/RequestGuardian";
import SafeModuleRecovery from "./components/SafeModuleRecovery";
import TriggerAccountRecovery from "./components/TriggerAccountRecovery";
import { STEPS } from "./constants";
import { Web3Provider } from "./providers/Web3Provider";
import { ConnectKitButton } from "connectkit";
import { useAccount } from "wagmi";
import { AppContextProvider } from "./context/AppContextProvider";
export const StepsContext = createContext(null);
function App() {
const [step, setStep] = useState(STEPS.CONNECT_WALLETS);
const renderBody = () => {
switch (step) {
case STEPS.CONNECT_WALLETS:
return <ConnectWallets />;
case STEPS.SAFE_MODULE_RECOVERY:
return <SafeModuleRecovery />;
case STEPS.REQUEST_GUARDIAN:
return <RequestGuardian />;
case STEPS.REQUESTED_RECOVERIES:
return <RequestedRecoveries />;
case STEPS.TRIGGER_ACCOUNT_RECOVERY:
return <TriggerAccountRecovery />;
default:
return <ConnectWallets />;
}
};
return (
<Web3Provider>
<AppContextProvider>
<StepsContext.Provider
value={{
setStep,
}}
>
<div className="app">
<Navbar />
<h1>Safe Email Recovery Demo</h1>
{renderBody()}
</div>
</StepsContext.Provider>{" "}
</AppContextProvider>
</Web3Provider>
);
}
export default App;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,3 @@
<svg width="23" height="20" viewBox="0 0 23 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M21.2 9.5L19.2005 11.5L17.2 9.5M19.4451 11C19.4814 10.6717 19.5 10.338 19.5 10C19.5 5.02944 15.4706 1 10.5 1C5.52944 1 1.5 5.02944 1.5 10C1.5 14.9706 5.52944 19 10.5 19C13.3273 19 15.85 17.6963 17.5 15.6573M10.5 5V10L13.5 12" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 419 B

View File

@@ -0,0 +1,3 @@
<svg width="21" height="20" viewBox="0 0 21 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.5 19H7.5M13.5 19H16.5M16 4.5V12.5M1.5 4.2L1.5 12.8C1.5 13.9201 1.5 14.4802 1.71799 14.908C1.90973 15.2843 2.21569 15.5903 2.59202 15.782C3.01984 16 3.57989 16 4.7 16L16.3 16C17.4201 16 17.9802 16 18.408 15.782C18.7843 15.5903 19.0903 15.2843 19.282 14.908C19.5 14.4802 19.5 13.9201 19.5 12.8V4.2C19.5 3.0799 19.5 2.51984 19.282 2.09202C19.0903 1.7157 18.7843 1.40974 18.408 1.21799C17.9802 1 17.4201 1 16.3 1L4.7 1C3.5799 1 3.01984 1 2.59202 1.21799C2.2157 1.40973 1.90973 1.71569 1.71799 2.09202C1.5 2.51984 1.5 3.07989 1.5 4.2ZM10 8.5C10 9.88071 8.88071 11 7.5 11C6.11929 11 5 9.88071 5 8.5C5 7.11929 6.11929 6 7.5 6C8.88071 6 10 7.11929 10 8.5Z" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 845 B

View File

@@ -0,0 +1,3 @@
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11 15V11M11 7H11.01M21 11C21 16.5228 16.5228 21 11 21C5.47715 21 1 16.5228 1 11C1 5.47715 5.47715 1 11 1C16.5228 1 21 5.47715 21 11Z" stroke="#2E90FA" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 330 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@@ -0,0 +1,17 @@
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<g filter="url(#filter0_d_2084_199)">
<path d="M4.60648 7.25C4.76258 7.54725 5.07428 7.75 5.43333 7.75H6.5C7.05228 7.75 7.5 7.30229 7.5 6.75C7.5 6.19772 7.05228 5.75 6.5 5.75H5.5C4.94772 5.75 4.5 5.30229 4.5 4.75C4.5 4.19772 4.94772 3.75 5.5 3.75H6.56667C6.92572 3.75 7.23742 3.95275 7.39352 4.25M6 3V3.75M6 7.75V8.5M10 6C10 8.45422 7.32302 10.2392 6.349 10.8074C6.2383 10.872 6.18295 10.9043 6.10484 10.9211C6.04422 10.9341 5.95578 10.9341 5.89516 10.9211C5.81705 10.9043 5.7617 10.872 5.65101 10.8074C4.67698 10.2392 2 8.45422 2 6V3.6088C2 3.20904 2 3.00917 2.06538 2.83735C2.12314 2.68557 2.21699 2.55014 2.33883 2.44277C2.47675 2.32122 2.6639 2.25104 3.0382 2.11067L5.7191 1.10534C5.82305 1.06636 5.87502 1.04687 5.92849 1.03914C5.97592 1.03229 6.02408 1.03229 6.07151 1.03914C6.12498 1.04687 6.17695 1.06636 6.2809 1.10534L8.9618 2.11067C9.3361 2.25104 9.52325 2.32122 9.66117 2.44277C9.78301 2.55014 9.87686 2.68557 9.93462 2.83735C10 3.00917 10 3.20904 10 3.6088V6Z" stroke="#F79009" stroke-linecap="round" stroke-linejoin="round" shape-rendering="crispEdges"/>
</g>
<defs>
<filter id="filter0_d_2084_199" x="-2.5" y="0.534" width="17" height="18.8968" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="4"/>
<feGaussianBlur stdDeviation="2"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_2084_199"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_2084_199" result="shape"/>
</filter>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14.5 12H14.51M1 3V17C1 18.1046 1.89543 19 3 19H17C18.1046 19 19 18.1046 19 17V7C19 5.89543 18.1046 5 17 5L3 5C1.89543 5 1 4.10457 1 3ZM1 3C1 1.89543 1.89543 1 3 1H15M15 12C15 12.2761 14.7761 12.5 14.5 12.5C14.2239 12.5 14 12.2761 14 12C14 11.7239 14.2239 11.5 14.5 11.5C14.7761 11.5 15 11.7239 15 12Z" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 496 B

Binary file not shown.

View File

@@ -0,0 +1,18 @@
import React, { ReactNode } from "react";
type ButtonProps = {
endIcon?: ReactNode;
loading?: boolean;
} & React.ComponentPropsWithoutRef<"button">;
export function Button({ children, ...buttonProps }: ButtonProps) {
return (
<div className="button">
<button {...buttonProps}>
{children}
{buttonProps?.endIcon ? buttonProps?.endIcon : null}
{buttonProps?.loading ? <div className="loader" /> : null}
</button>
</div>
);
}

View File

@@ -0,0 +1,173 @@
import { useState, useCallback, useMemo } from 'react'
import { useAccount, useWriteContract, useReadContract } from 'wagmi'
import { abi as safeAbi } from '../abi/Safe.json'
import { abi as recoveryPluginAbi } from '../abi/SafeZkEmailRecoveryPlugin.json'
import { safeZkSafeZkEmailRecoveryPlugin } from '../../contracts.base-sepolia.json'
import { Button } from './Button'
import { genAccountCode, getRequestGuardianSubject, templateIdx } from '../utils/email'
import { readContract } from 'wagmi/actions'
import { config } from '../providers/config'
import { pad } from 'viem'
import { relayer } from '../services/relayer'
import { useAppContext } from '../context/AppContextHook'
export function ConfigureSafeModule() {
const { address } = useAccount()
const { writeContractAsync } = useWriteContract()
const {
guardianEmail,
setGuardianEmail,
accountCode,
setAccountCode
} = useAppContext()
// TODO 0 sets recovery to default of 2 weeks, likely want a warning here
// Also, better time duration setting component
const [recoveryDelay, setRecoveryDelay] = useState(0)
const { data: isModuleEnabled } = useReadContract({
address,
abi: safeAbi,
functionName: 'isModuleEnabled',
args: [safeZkSafeZkEmailRecoveryPlugin]
});
const { data: safeOwnersData } = useReadContract({
address,
abi: safeAbi,
functionName: 'getOwners',
});
const firstSafeOwner = useMemo(() => {
const safeOwners = safeOwnersData as string[];
if (!safeOwners?.length) {
return;
}
return safeOwners[0];
}, [safeOwnersData]);
// const checkGuardianAcceptance = useCallback(async () => {
// if (!gurdianRequestId) {
// throw new Error('missing guardian request id')
// }
// const resBody = await relayer.requestStatus(gurdianRequestId)
// console.debug('guardian req res body', resBody);
// }, [gurdianRequestId])
const enableEmailRecoveryModule = useCallback(async () => {
if (!address) {
throw new Error('unable to get account address');
}
await writeContractAsync({
abi: safeAbi,
address,
functionName: 'enableModule',
args: [safeZkSafeZkEmailRecoveryPlugin],
})
}, [address, writeContractAsync])
const configureRecoveryAndRequestGuardian = useCallback(async () => {
if (!address) {
throw new Error('unable to get account address');
}
if (!guardianEmail) {
throw new Error('guardian email not set')
}
if (!firstSafeOwner) {
throw new Error('safe owner not found')
}
const acctCode = await genAccountCode();
setAccountCode(accountCode);
const guardianSalt = await relayer.getAccountSalt(acctCode, guardianEmail);
const guardianAddr = await readContract(config, {
abi: recoveryPluginAbi,
address: safeZkSafeZkEmailRecoveryPlugin as `0x${string}`,
functionName: 'computeEmailAuthAddress',
args: [guardianSalt]
})
// TODO Should this be something else?
const previousOwnerInLinkedList = pad("0x1", {
size: 20
})
await writeContractAsync({
abi: recoveryPluginAbi,
address: safeZkSafeZkEmailRecoveryPlugin as `0x${string}`,
functionName: 'configureRecovery',
args: [
firstSafeOwner,
guardianAddr,
recoveryDelay,
previousOwnerInLinkedList
],
})
console.debug('recovery configured');
const recoveryRouterAddr = await readContract(config, {
abi: recoveryPluginAbi,
address: safeZkSafeZkEmailRecoveryPlugin as `0x${string}`,
functionName: 'getRouterForSafe',
args: [address]
}) as string;
const subject = getRequestGuardianSubject(address);
const { requestId } = await relayer.acceptanceRequest(
recoveryRouterAddr,
guardianEmail,
acctCode,
templateIdx,
subject,
);
console.debug('req guard req id', requestId)
// TODO poll until guard req is complete or fails
}, [
address,
firstSafeOwner,
guardianEmail,
recoveryDelay,
accountCode,
setAccountCode,
writeContractAsync
])
return (
<>
{
isModuleEnabled ?
<div>Recovery Module Enabled</div> :
<Button onClick={enableEmailRecoveryModule}>
1. Enable Email Recovery Module
</Button>
}
<div>
<label>
Guardian's Email
<input disabled ={!isModuleEnabled}
type='email'
onInput={e => setGuardianEmail((e.target as HTMLTextAreaElement).value)}
/>
</label>
<label>
Recovery Delay
<input
disabled={!isModuleEnabled}
type='number'
onInput={e => setRecoveryDelay(parseInt((e.target as HTMLTextAreaElement).value))}
/>
</label>
<Button
disabled={!isModuleEnabled}
onClick={configureRecoveryAndRequestGuardian}>
2. Configure Recovery & Request Guardian
</Button>
</div>
</>
);
}

View File

@@ -0,0 +1,42 @@
import { Button } from "./Button";
import walletIcon from "../assets/wallet.svg";
import { ConnectKitButton } from "connectkit";
import { useAccount } from "wagmi";
import { useContext } from "react";
import { StepsContext } from "../App";
import { STEPS } from "../constants";
const ConnectWallets = () => {
const { address } = useAccount();
const stepsContext = useContext(StepsContext);
if (address) {
console.log(stepsContext);
stepsContext?.setStep(STEPS.SAFE_MODULE_RECOVERY);
}
return (
<div className="connect-wallets-container">
{/* <Button endIcon={<img src={walletIcon} />}>Connect Genosis Safe</Button>
<p color="#CECFD2" style={{ display: "flex", gap: "0.5rem" }}>
<img src={infoIcon} alt="info" />
Copy the link and import into your safe wallet
</p> */}
<ConnectKitButton.Custom>
{({ show }) => {
return (
<Button onClick={show} endIcon={<img src={walletIcon} />}>
Connect Safe
</Button>
);
}}
</ConnectKitButton.Custom>
{/* <p style={{ textDecoration: "underline" }}>
Or, recover existing wallet instead ➔
</p> */}
</div>
);
};
export default ConnectWallets;

View File

@@ -0,0 +1,13 @@
import { Web3Provider } from "../providers/Web3Provider";
import { ConnectKitButton } from "connectkit";
import { Button } from "./Button";
const Navbar = () => {
return (
<nav className="navbar">
<ConnectKitButton />
</nav>
);
};
export default Navbar;

View File

@@ -0,0 +1,94 @@
import { useState, useCallback } from 'react'
import { Button } from './Button'
import { relayer } from '../services/relayer'
import { abi as recoveryPluginAbi } from '../abi/SafeZkEmailRecoveryPlugin.json'
import { useReadContract, useAccount } from 'wagmi'
import {
getRequestsRecoverySubject,
templateIdx
} from '../utils/email'
import { safeZkSafeZkEmailRecoveryPlugin } from '../../contracts.base-sepolia.json'
import { useAppContext } from '../context/AppContextHook'
export function PerformRecovery() {
const { address } = useAccount()
const { guardianEmail } = useAppContext()
const [newOwner, setNewOwner] = useState<string>()
// TODO pull from recovery module
// const { data: timelock } = useReadContract({
// address: simpleWalletAddress as HexStr,
// abi: simpleWalletAbi,
// functionName: 'timelock',
// });
const { data: recoveryRouterAddr } = useReadContract({
abi: recoveryPluginAbi,
address: safeZkSafeZkEmailRecoveryPlugin as `0x${string}`,
functionName: 'getRouterForSafe',
args: [address]
});
const requestRecovery = useCallback(async () => {
if (!address) {
throw new Error('unable to get account address');
}
if (!guardianEmail) {
throw new Error('guardian email not set')
}
if (!newOwner) {
throw new Error('new owner not set')
}
if (!recoveryRouterAddr) {
throw new Error('could not find recovery router for safe')
}
const subject = getRequestsRecoverySubject(address, newOwner)
const { requestId } = await relayer.recoveryRequest(
recoveryRouterAddr as string,
guardianEmail,
templateIdx,
subject,
)
console.debug('recovery request id', requestId)
}, [recoveryRouterAddr, address, guardianEmail, newOwner])
const completeRecovery = useCallback(async () => {
if (!recoveryRouterAddr) {
throw new Error('could not find recovery router for safe')
}
console.debug('recovery router addr', recoveryRouterAddr);
const res = relayer.completeRecovery(
recoveryRouterAddr as string
);
console.debug('complete recovery res', res)
}, [recoveryRouterAddr]);
return (
<>
<label>
New Owner (address)
<input type='text'
onInput={e => setNewOwner((e.target as HTMLTextAreaElement).value)}
/>
</label>
<Button onClick={requestRecovery}>
3. Request Recovery
</Button>
{/* <div>{`TEST timelock: ${timelock}`}</div> */}
<Button onClick={completeRecovery}>
TEST Complete Recovery (Switch to polling)
</Button>
</>
);
}

View File

@@ -0,0 +1,220 @@
import { useCallback, useContext, useMemo, useState } from "react";
import { ConnectKitButton } from "connectkit";
import { Button } from "./Button";
import { useAccount, useReadContract, useWriteContract } from "wagmi";
import { abi as safeAbi } from "../abi/Safe.json";
import { useAppContext } from "../context/AppContextHook";
import { abi as recoveryPluginAbi } from "../abi/SafeZkEmailRecoveryPlugin.json";
import { safeZkSafeZkEmailRecoveryPlugin } from "../../contracts.base-sepolia.json";
import {
genAccountCode,
getRequestGuardianSubject,
templateIdx,
} from "../utils/email";
import { readContract } from "wagmi/actions";
import { config } from "../providers/config";
import { pad } from "viem";
import { relayer } from "../services/relayer";
import { StepsContext } from "../App";
import { STEPS } from "../constants";
const RequestGuardian = () => {
const { address } = useAccount();
const { writeContractAsync } = useWriteContract();
const { guardianEmail, setGuardianEmail, accountCode, setAccountCode } =
useAppContext();
const stepsContext = useContext(StepsContext);
const [loading, setLoading] = useState(false);
// 0 = 2 week default delay, don't do for demo
const [recoveryDelay, setRecoveryDelay] = useState(1);
const isMobile = window.innerWidth < 768;
const { data: safeOwnersData } = useReadContract({
address,
abi: safeAbi,
functionName: "getOwners",
});
const firstSafeOwner = useMemo(() => {
const safeOwners = safeOwnersData as string[];
if (!safeOwners?.length) {
return;
}
return safeOwners[0];
}, [safeOwnersData]);
const configureRecoveryAndRequestGuardian = useCallback(async () => {
if (!address) {
throw new Error("unable to get account address");
}
if (!guardianEmail) {
throw new Error("guardian email not set");
}
if (!firstSafeOwner) {
throw new Error("safe owner not found");
}
try {
setLoading(true);
const acctCode = await genAccountCode();
setAccountCode(accountCode);
const guardianSalt = await relayer.getAccountSalt(acctCode, guardianEmail);
const guardianAddr = await readContract(config, {
abi: recoveryPluginAbi,
address: safeZkSafeZkEmailRecoveryPlugin as `0x${string}`,
functionName: "computeEmailAuthAddress",
args: [guardianSalt],
});
// TODO Should this be something else?
const previousOwnerInLinkedList = pad("0x1", {
size: 20,
});
await writeContractAsync({
abi: recoveryPluginAbi,
address: safeZkSafeZkEmailRecoveryPlugin as `0x${string}`,
functionName: "configureRecovery",
args: [
firstSafeOwner,
guardianAddr,
recoveryDelay,
previousOwnerInLinkedList,
],
});
console.debug("recovery configured");
const recoveryRouterAddr = (await readContract(config, {
abi: recoveryPluginAbi,
address: safeZkSafeZkEmailRecoveryPlugin as `0x${string}`,
functionName: "getRouterForSafe",
args: [address],
})) as string;
const subject = getRequestGuardianSubject(address);
const { requestId } = await relayer.acceptanceRequest(
recoveryRouterAddr,
guardianEmail,
acctCode,
templateIdx,
subject
);
console.debug('accept req id', requestId);
// TODO Use polling instead
stepsContext?.setStep(STEPS.REQUESTED_RECOVERIES);
// let checkGuardianAcceptanceInterval = null
// const checkGuardianAcceptance = async () => {
// if (!requestId) {
// throw new Error("missing guardian request id");
// }
// const resBody = await relayer.requestStatus(requestId);
// console.debug("guardian req res body", resBody);
// if(resBody?.is_success) {
// stepsContext?.setStep(STEPS.REQUESTED_RECOVERIES);
// checkGuardianAcceptanceInterval?.clearInterval()
// }
// }
// checkGuardianAcceptanceInterval = setInterval(async () => {
// const res = await checkGuardianAcceptance();
// console.log(res)
// }, 5000);
} catch (err) {
console.error(err);
} finally {
setLoading(false);
}
}, [
address,
firstSafeOwner,
guardianEmail,
recoveryDelay,
accountCode,
setAccountCode,
writeContractAsync,
]);
return (
<div
style={{
maxWidth: isMobile ? "100%" : "50%",
width: "100%",
display: "flex",
flexDirection: "column",
justifyContent: "center",
alignItems: "flex-start",
gap: "2rem",
}}
>
<div style={{ display: "flex", flexDirection: "column", gap: "1rem" }}>
Connected wallet:
<ConnectKitButton />
</div>
<div
style={{
display: "flex",
flexDirection: "column",
gap: "1rem",
width: "100%",
}}
>
Guardian Details:
<div className="container">
<div
style={{
display: "flex",
flexDirection: "row",
gap: "2rem",
width: "100%",
alignItems: "flex-end",
flexWrap: "wrap",
justifyContent: "space-between",
}}
>
<div
style={{
display: "flex",
flexDirection: "column",
width: isMobile ? "90%" : "60%",
}}
>
<p>Guardian's Email</p>
<input
style={{ width: "100%" }}
type="email"
value={guardianEmail}
onChange={(e) => setGuardianEmail(e.target.value)}
/>
</div>
<div>
<span>Recovery Delay (seconds)</span>
<input
style={{ width: "1.875rem", marginLeft: "1rem" }}
type="number"
min={1}
value={recoveryDelay}
onChange={(e) => setRecoveryDelay(e.target.value)}
/>
</div>
</div>
</div>
</div>
<div style={{ margin: "auto" }}>
<Button loading={loading} onClick={configureRecoveryAndRequestGuardian}>
Configure Recovery and Request Guardian
</Button>
</div>
</div>
);
};
export default RequestGuardian;

View File

@@ -0,0 +1,261 @@
import { useCallback, useContext, useState } from "react";
import { Web3Provider } from "../providers/Web3Provider";
import { ConnectKitButton } from "connectkit";
import { Button } from "./Button";
import cancelRecoveryIcon from "../assets/cancelRecoveryIcon.svg";
import completeRecoveryIcon from "../assets/completeRecoveryIcon.svg";
import recoveredIcon from "../assets/recoveredIcon.svg";
import { useAppContext } from "../context/AppContextHook";
import { useAccount, useReadContract } from "wagmi";
import { relayer } from "../services/relayer";
import { abi as recoveryPluginAbi } from "../abi/SafeZkEmailRecoveryPlugin.json";
import { getRequestsRecoverySubject, templateIdx } from "../utils/email";
import { safeZkSafeZkEmailRecoveryPlugin } from "../../contracts.base-sepolia.json";
import { StepsContext } from "../App";
import { STEPS } from "../constants";
const BUTTON_STATES = {
TRIGGER_RECOVERY: "Trigger Recovery",
CANCEL_RECOVERY: "Cancel Recovery",
COMPLETE_RECOVERY: "Complete Recovery",
RECOVERY_COMPLETED: "Recovery Completed",
};
const RequestedRecoveries = () => {
const isMobile = window.innerWidth < 768;
const { address } = useAccount();
const { guardianEmail } = useAppContext();
const stepsContext = useContext(StepsContext);
const [newOwner, setNewOwner] = useState<string>();
const [buttonState, setButtonState] = useState(
BUTTON_STATES.TRIGGER_RECOVERY,
);
const [loading, setLoading] = useState<boolean>(false);
const [gurdianRequestId, setGuardianRequestId] = useState<number>();
const { data: recoveryRouterAddr } = useReadContract({
abi: recoveryPluginAbi,
address: safeZkSafeZkEmailRecoveryPlugin as `0x${string}`,
functionName: "getRouterForSafe",
args: [address],
});
const requestRecovery = useCallback(async () => {
setLoading(true);
if (!address) {
throw new Error("unable to get account address");
}
if (!guardianEmail) {
throw new Error("guardian email not set");
}
if (!newOwner) {
throw new Error("new owner not set");
}
if (!recoveryRouterAddr) {
throw new Error("could not find recovery router for safe");
}
const subject = getRequestsRecoverySubject(address, newOwner);
const { requestId } = await relayer.recoveryRequest(
recoveryRouterAddr as string,
guardianEmail,
templateIdx,
subject,
);
setGuardianRequestId(requestId);
setLoading(false);
setButtonState(BUTTON_STATES.COMPLETE_RECOVERY);
// let checkRequestRecoveryStatusInterval = null
// const checkGuardianAcceptance = async () => {
// if (!requestId) {
// throw new Error("missing guardian request id");
// }
// const resBody = await relayer.requestStatus(requestId);
// console.debug("guardian req res body", resBody);
// if(resBody?.is_success) {
// setLoading(false);
// setButtonState(BUTTON_STATES.COMPLETE_RECOVERY);
// checkRequestRecoveryStatusInterval?.clearInterval()
// }
// }
// checkRequestRecoveryStatusInterval = setInterval(async () => {
// const res = await checkGuardianAcceptance();
// console.log(res)
// }, 5000);
}, [recoveryRouterAddr, address, guardianEmail, newOwner]);
const completeRecovery = useCallback(async () => {
setLoading(true);
if (!recoveryRouterAddr) {
throw new Error("could not find recovery router for safe");
}
const res = relayer.completeRecovery(recoveryRouterAddr as string);
console.debug("complete recovery res", res);
setLoading(false);
setButtonState(BUTTON_STATES.RECOVERY_COMPLETED);
}, [recoveryRouterAddr]);
// const checkGuardianAcceptance = useCallback(async () => {
// if (!gurdianRequestId) {
// throw new Error("missing guardian request id");
// }
// const resBody = await relayer.requestStatus(gurdianRequestId);
// console.debug("guardian req res body", resBody);
// }, [gurdianRequestId]);
const getButtonComponent = () => {
switch (buttonState) {
case BUTTON_STATES.TRIGGER_RECOVERY:
return (
<Button loading={loading} onClick={requestRecovery}>
Trigger Recovery
</Button>
);
case BUTTON_STATES.CANCEL_RECOVERY:
return (
<Button endIcon={<img src={cancelRecoveryIcon} />}>
Cancel Recovery
</Button>
);
case BUTTON_STATES.COMPLETE_RECOVERY:
return (
<Button
loading={loading}
onClick={completeRecovery}
endIcon={<img src={completeRecoveryIcon} />}
>
Complete Recovery
</Button>
);
case BUTTON_STATES.RECOVERY_COMPLETED:
return (
<Button
loading={loading}
onClick={() => stepsContext.setStep(STEPS.CONNECT_WALLETS)}
>
Complete! Connect new wallet to set new guardians
</Button>
);
}
};
return (
<div
style={{
maxWidth: isMobile ? "100%" : "50%",
width: "100%",
display: "flex",
flexDirection: "column",
justifyContent: "center",
alignItems: "flex-start",
gap: "2rem",
}}
>
<div style={{ display: "flex", flexDirection: "column", gap: "1rem" }}>
Connected wallet:
<div
style={{
display: "flex",
flexDirection: "row",
justifyContent: "center",
alignItems: "center",
gap: "1rem",
}}
>
<ConnectKitButton />
{buttonState === BUTTON_STATES.RECOVERY_COMPLETED ? (
<div
style={{
background: "#4E1D09",
border: "1px solid #93370D",
color: "#FEC84B",
padding: "0.25rem 0.75rem",
borderRadius: "3.125rem",
width: "fit-content",
height: "fit-content",
}}
>
Recovered
<img src={recoveredIcon} style={{ marginRight: "0.5rem" }} />
</div>
) : null}
</div>
</div>
{buttonState === BUTTON_STATES.RECOVERY_COMPLETED ? null : (
<div
style={{
display: "flex",
flexDirection: "column",
gap: "1rem",
width: "100%",
}}
>
Requested Recoveries:
<div className="container">
<div
style={{
display: "flex",
flexDirection: "row",
gap: isMobile ? "1rem" : "3rem",
width: "100%",
alignItems: "flex-end",
flexWrap: "wrap",
}}
>
<div
style={{
display: "flex",
flexDirection: "column",
width: isMobile ? "90%" : "45%",
}}
>
<p>Guardian's Email</p>
<input
style={{ width: "100%" }}
type="email"
value={guardianEmail}
readOnly={true}
/>
</div>
<div
style={{
display: "flex",
flexDirection: "column",
width: isMobile ? "90%" : "45%",
}}
>
<p>Requested New Wallet Address</p>
<input
style={{ width: "100%" }}
type="email"
value={newOwner}
onChange={(e) => setNewOwner(e.target.value)}
/>
</div>
</div>
</div>
</div>
)}
<div style={{ margin: "auto" }}>{getButtonComponent()}</div>
</div>
);
};
export default RequestedRecoveries;

View File

@@ -0,0 +1,65 @@
import { ConnectKitButton } from "connectkit";
import { Button } from "./Button";
import { useAccount, useReadContract, useWriteContract } from "wagmi";
import { safeZkSafeZkEmailRecoveryPlugin } from "../../contracts.base-sepolia.json";
import { abi as safeAbi } from "../abi/Safe.json";
import { useCallback, useContext, useEffect, useState } from "react";
import { StepsContext } from "../App";
import { STEPS } from "../constants";
const SafeModuleRecovery = () => {
const { address } = useAccount();
const { writeContractAsync } = useWriteContract();
const stepsContext = useContext(StepsContext);
const [loading, setLoading] = useState(false);
useEffect(() => {
if (!address) {
stepsContext?.setStep(STEPS.CONNECT_WALLETS);
}
}, [address, stepsContext]);
const { data: isModuleEnabled } = useReadContract({
address,
abi: safeAbi,
functionName: "isModuleEnabled",
args: [safeZkSafeZkEmailRecoveryPlugin],
});
console.log(isModuleEnabled);
if (isModuleEnabled) {
console.log("Module is enabled");
setLoading(false);
stepsContext?.setStep(STEPS.REQUEST_GUARDIAN);
}
const enableEmailRecoveryModule = useCallback(async () => {
setLoading(true);
if (!address) {
throw new Error("unable to get account address");
}
await writeContractAsync({
abi: safeAbi,
address,
functionName: "enableModule",
args: [safeZkSafeZkEmailRecoveryPlugin],
});
}, [address, writeContractAsync]);
return (
<div style={{ display: "flex", gap: "2rem", flexDirection: "column" }}>
<div style={{ display: "flex", alignItems: "center", gap: "1rem" }}>
Connected wallet: <ConnectKitButton />
</div>
{!isModuleEnabled ? (
<Button disabled={loading} onClick={enableEmailRecoveryModule}>
Enable Email Recovery Module
</Button>
) : null}
</div>
);
};
export default SafeModuleRecovery;

View File

@@ -0,0 +1,126 @@
import { useState } from "react";
import { Web3Provider } from "../providers/Web3Provider";
import { ConnectKitButton } from "connectkit";
import { Button } from "./Button";
import cancelRecoveryIcon from "../assets/cancelRecoveryIcon.svg";
import completeRecoveryIcon from "../assets/completeRecoveryIcon.svg";
const BUTTON_STATES = {
CANCEL_RECOVERY: "Cancel Recovery",
COMPLETE_RECOVERY: "Complete Recovery",
};
const TriggerAccountRecovery = () => {
const isMobile = window.innerWidth < 768;
const [guardianEmail, setGuardianEmail] = useState("");
const [newWalletAddress, setNewWalletAddress] = useState("");
const [buttonState, setButtonState] = useState(BUTTON_STATES.CANCEL_RECOVERY);
return (
<Web3Provider>
<div
style={{
maxWidth: isMobile ? "100%" : "50%",
width: "100%",
display: "flex",
flexDirection: "column",
justifyContent: "center",
alignItems: "flex-start",
gap: "2rem",
}}
>
<div style={{ display: "flex", flexDirection: "column", gap: "1rem" }}>
Connected wallet:
<ConnectKitButton />
</div>
<div
style={{
display: "flex",
flexDirection: "column",
gap: "1rem",
width: "100%",
}}
>
Triggered Account Recoveries:
<div className="container">
<div
style={{
display: "flex",
flexDirection: "row",
gap: isMobile ? "1rem" : "3rem",
width: "100%",
alignItems: "flex-end",
flexWrap: "wrap",
}}
>
<div
style={{
display: "flex",
flexDirection: "column",
width: isMobile ? "90%" : "45%",
}}
>
<p>Guardian's Email</p>
<input
style={{ width: "100%" }}
type="email"
value={guardianEmail}
onChange={(e) => setGuardianEmail(e.target.value)}
/>
</div>
<div
style={{
display: "flex",
flexDirection: "column",
width: isMobile ? "90%" : "45%",
}}
>
<p>Previous Wallet Address</p>
<input
style={{ width: "100%" }}
type="email"
value={guardianEmail}
onChange={(e) => setGuardianEmail(e.target.value)}
/>
</div>
<div
style={{
display: "flex",
flexDirection: "column",
width: isMobile ? "90%" : "45%",
}}
>
<p>New Wallet Address</p>
<input
style={{ width: "100%" }}
type="email"
value={newWalletAddress}
onChange={(e) => setNewWalletAddress(e.target.value)}
/>
</div>
</div>
</div>
</div>
<div style={{ margin: "auto" }}>
<Button
endIcon={
buttonState === BUTTON_STATES.CANCEL_RECOVERY ? (
<img src={cancelRecoveryIcon} />
) : (
<img src={completeRecoveryIcon} />
)
}
>
{buttonState === BUTTON_STATES.CANCEL_RECOVERY
? "Cancel "
: "Complete"}
Recovery
</Button>
</div>
</div>
</Web3Provider>
);
};
export default TriggerAccountRecovery;

View File

@@ -0,0 +1,7 @@
export const STEPS = {
CONNECT_WALLETS: 0,
SAFE_MODULE_RECOVERY: 1,
REQUEST_GUARDIAN: 2,
REQUESTED_RECOVERIES: 3,
TRIGGER_ACCOUNT_RECOVERY: 4,
};

View File

@@ -0,0 +1,16 @@
import { createContext } from 'react'
type AppContextType = {
accountCode: string,
setAccountCode: (ac: string) => void;
guardianEmail: string;
setGuardianEmail: (ge: string) => void;
}
export const appContext = createContext<AppContextType>({
accountCode: '',
setAccountCode: () => {},
guardianEmail: '',
setGuardianEmail: () => {}
});

View File

@@ -0,0 +1,4 @@
import { useContext } from "react";
import { appContext } from "./AppContext";
export const useAppContext = () => useContext(appContext)

View File

@@ -0,0 +1,23 @@
import { ReactNode, useMemo, useState } from "react";
import { appContext } from "./AppContext";
export const AppContextProvider = ({ children } : { children: ReactNode }) => {
const [accountCode, setAccountCode] = useState('');
const [guardianEmail, setGuardianEmail] = useState('');
const ctxVal = useMemo(() => ({
accountCode,
setAccountCode,
guardianEmail,
setGuardianEmail,
}), [
accountCode,
guardianEmail
])
return (
<appContext.Provider value={ctxVal}>
{children}
</appContext.Provider>
)
}

View File

@@ -0,0 +1,153 @@
:root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #0C111D;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
body {
margin: 0;
}
.app {
display: flex;
padding: 0 2rem;
flex-direction: column;
align-items: center;
justify-content: center;
color: #fff;
height: 100vh;
}
h1 {
font-size: 2.25rem;
line-height: 1.1;
text-align: center;
font-weight: 600;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
display: flex;
gap: 1rem;
font-weight: 600;
font-family: inherit;
cursor: pointer;
transition: border-color 0.25s;
border: none;
box-shadow: 0px 1px 2px 0px #1018280D;
background: linear-gradient(354.6deg, #0069E4 37.48%, #37C3FF 107.66%);
border-image-source: linear-gradient(144.35deg, #0069E4 33.65%, #37C3FF 93.17%);
padding: 22px;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
/* outline: 4px auto -webkit-focus-ring-color; */
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}
.navbar {
position: absolute;
top: 1.25rem;
right: 1.25rem;
width: 100vw;
display: flex;
flex-direction: row;
justify-content: flex-end;
}
.connect-wallets-container {
display: flex;
gap: 1rem;
margin-top: 1rem;
flex-direction: column;
width: fit-content;
align-items: center;
}
input {
background: var(--Colors-Background-bg-tertiary, #1F242F);
border: 1px solid var(--Colors-Border-border-primary, #333741);
box-shadow: 0px 1px 2px 0px #1018280D;
border-radius: 4px;
padding: 8px 12px;
color: #85888E;
}
.container {
display: flex;
flex-direction: column;
gap: 1rem;
align-items: center;
background: var(--Colors-Background-bg-secondary, #161B26);
border: 1px solid var(--Colors-Border-border-primary, #333741);
padding: 20px 24px 20px 24px;
gap: 20px;
border-radius: 12px;
border: 1px 0px 0px 0px;
opacity: 0px;
color: #94969C;
font-weight: 500;
}
.loader {
border: 2px solid #f3f3f3;
border-top: 2px solid #3498db;
border-radius: 50%;
width: 12px;
height: 12px;
animation: spin 2s linear infinite;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}

View File

@@ -0,0 +1,10 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.tsx'
import './index.css'
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>,
)

View File

@@ -0,0 +1,24 @@
import { ReactNode } from "react";
import { WagmiProvider } from "wagmi";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ConnectKitProvider } from "connectkit";
import { config } from "./config";
const connectKitOptions = {
walletConnectName: 'WalletConnect',
hideNoWalletCTA: true,
};
const queryClient = new QueryClient();
export const Web3Provider = ({ children }: { children: ReactNode }) => {
return (
<WagmiProvider config={config}>
<QueryClientProvider client={queryClient}>
<ConnectKitProvider options={connectKitOptions}>
{children}
</ConnectKitProvider>
</QueryClientProvider>
</WagmiProvider>
);
};

View File

@@ -0,0 +1,15 @@
import { getDefaultConfig } from "connectkit";
import { baseSepolia } from "viem/chains";
import { createConfig } from "wagmi";
// TODO Consider https://wagmi.sh/core/api/connectors/safe
export const config = createConfig(
getDefaultConfig({
chains: [baseSepolia], // TODO Update with non-public prc endpoint
walletConnectProjectId: import.meta.env.VITE_WALLET_CONNECT_PROJECT_ID,
appName: "Safe Email Recovery Demo",
appDescription: "Safe Email Recovery Demo",
appUrl: window.location.origin,
appIcon: "https://i.imgur.com/46VRTCF.png",
}),
);

View File

@@ -0,0 +1,99 @@
import axios from "axios"
// Spec: https://www.notion.so/proofofemail/Email-Sender-Auth-c87063cd6cdc4c5987ea3bc881c68813#d7407d31e1354167be61612f5a16995b
// TODO Consider using a bigint for templateIdx as it *could* overflow JS number, but practically seems unlikely
class Relayer {
private readonly apiRoute = 'api';
apiUrl: string;
constructor(relayerUrl: string) {
this.apiUrl = `${relayerUrl}${this.apiRoute}`
}
// Similar to a ping or health endpoint
async echo() {
const res = await axios({
method: 'GET',
url: `${this.apiUrl}/echo`
})
return res.data;
}
async requestStatus(requestId: number) {
const { data } = await axios({
method: 'POST',
url: `${this.apiUrl}/requestStatus`,
data: {
request_id: requestId
}
})
return data;
}
async acceptanceRequest(
walletEthAddr: string,
guardianEmailAddr: string,
accountCode: string,
templateIdx: number,
subject: string
): Promise<{ requestId: number }> {
const { data } = await axios({
method: "POST",
url: `${this.apiUrl}/acceptanceRequest`,
data: {
wallet_eth_addr: walletEthAddr,
guardian_email_addr: guardianEmailAddr,
account_code: accountCode,
template_idx: templateIdx,
subject,
}
})
const { request_id: requestId } = data;
return { requestId };
}
async recoveryRequest(
walletEthAddr: string,
guardianEmailAddr: string,
templateIdx: number,
subject: string
) {
const { data } = await axios({
method: "POST",
url: `${this.apiUrl}/recoveryRequest`,
data: {
wallet_eth_addr: walletEthAddr,
guardian_email_addr: guardianEmailAddr,
template_idx: templateIdx,
subject,
}
})
const { request_id: requestId } = data
return { requestId };
}
async completeRecovery(walletEthAddr: string) {
const data = await axios({
method: "POST",
url: `${this.apiUrl}/completeRecovery`,
data: {
wallet_eth_addr: walletEthAddr,
}
})
return data;
}
async getAccountSalt(accountCode: string, emailAddress: string) {
const { data } = await axios({
method: "POST",
url: `${this.apiUrl}/getAccountSalt`,
data: {
account_code: accountCode,
email_addr: emailAddress,
}
})
return data
}
}
export const relayer = new Relayer(import.meta.env.VITE_RELAYER_URL);

View File

@@ -0,0 +1,54 @@
import { buildPoseidon } from "circomlibjs";
export const templateIdx = 0
// From https://github.com/zkemail/email-wallet/blob/main/packages/frontend/src/components/RegisterUnclaim.tsx
// function padStringToBytes(str: string, len: number): Uint8Array {
// const bytes = new Uint8Array(len);
// const strBytes = (new TextEncoder).encode(str);
// bytes.set(strBytes);
// const empty = new Uint8Array(len - strBytes.length);
// bytes.set(empty, strBytes.length);
// return bytes;
// }
// function bytes2fields(bytes: Uint8Array, F: Poseidon['F']): bigint[] {
// const fields: bigint[] = [];
// for (let i = 0; i < bytes.length; i += 31) {
// const bytes32 = new Uint8Array(32);
// bytes32.set(bytes.slice(i, i + 31));
// const val = F.fromRprLE(bytes32, 0);
// fields.push(val);
// }
// return fields;
// }
export function bytesToHex(bytes: Uint8Array) {
return [...bytes]
.reverse()
.map(x => x.toString(16).padStart(2, "0"))
.join("");
}
export async function genAccountCode(): Promise<string> {
const poseidon = await buildPoseidon();
const accountCodeBytes: Uint8Array = poseidon.F.random();
return bytesToHex(accountCodeBytes);
}
// Use relayer.getAccountSalt instead
// export async function getGuardianSalt(guardianEmail: string, accountCode: Uint8Array) {
// const poseidon = await buildPoseidon();
// const emailField = bytes2fields(padStringToBytes(guardianEmail, 256), poseidon.F);
// const accountSaltBytes = poseidon([
// ...emailField, accountCode, 0
// ]);
// const accountSalt: `0x${string}` = `0x${bytesToHex(accountSaltBytes)}`
// return accountSalt;
// }
// TODO Update both with safe module accept subject
export const getRequestGuardianSubject = (acctAddr: string) =>
`Accept guardian request for ${acctAddr}`;
export const getRequestsRecoverySubject = (acctAddr: string, newOwner: string) =>
`Update owner to ${newOwner} on account ${acctAddr}`;

View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />

View File

@@ -0,0 +1,25 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}

View File

@@ -0,0 +1,11 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true,
"strict": true
},
"include": ["vite.config.ts"]
}

View File

@@ -0,0 +1,12 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { nodePolyfills } from 'vite-plugin-node-polyfills'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
nodePolyfills(),
react()
],
base: '/wax/'
})

View File

@@ -0,0 +1,19 @@
import { defineConfig } from '@wagmi/cli'
import { foundry, react } from '@wagmi/cli/plugins'
// TODO Fully link into project
export default defineConfig({
out: 'src/abis.ts',
plugins: [
foundry({
project: "../../plugins",
include: [
"EmailAccountRecovery.sol/**",
"Safe.sol/**",
"SafeZkEmailRecoveryPlugin.sol/**",
"SimpleWallet.sol/**",
],
}),
react()
],
})

File diff suppressed because it is too large Load Diff

View File

@@ -1,104 +1,104 @@
import jss from 'jss';
import color from 'color';
import React, { HTMLProps, useCallback, useState } from 'react';
import sheetsRegistry from './sheetsRegistry';
import { bgColor, dangerColor, fgColor } from './styleConstants';
import classes from './helpers/classes';
import runAsync from '../demo/helpers/runAsync';
import jss from "jss";
import color from "color";
import React, { HTMLProps, useCallback, useState } from "react";
import sheetsRegistry from "./sheetsRegistry";
import { bgColor, dangerColor, fgColor } from "./styleConstants";
import classes from "./helpers/classes";
import runAsync from "../demo/helpers/runAsync";
const sheet = jss.createStyleSheet({
Button: {
'& > .button-content': {
padding: '0.5em 1em',
"& > .button-content": {
padding: "0.5em 1em",
},
textAlign: 'center',
cursor: 'pointer',
userSelect: 'none',
textAlign: "center",
cursor: "pointer",
userSelect: "none",
background: color(fgColor).darken(0.1).toString(),
border: `1px solid ${color(fgColor).darken(0.1).toString()}`,
color: bgColor,
position: 'relative',
position: "relative",
'&:hover > .hover-error': {
display: 'inline-block',
"&:hover > .hover-error": {
display: "inline-block",
},
},
ButtonStates: {
'&:hover': {
"&:hover": {
background: fgColor,
border: `1px solid ${fgColor}`,
},
'&:active': {
background: 'white',
border: '1px solid white',
"&:active": {
background: "white",
border: "1px solid white",
},
},
ButtonSecondary: {
background: 'transparent',
background: "transparent",
border: `1px solid ${fgColor}`,
color: fgColor,
'& .loading-marker': {
"& .loading-marker": {
background: fgColor,
},
},
ButtonSecondaryStates: {
'&:hover': {
"&:hover": {
background: color(fgColor).alpha(0.05).toString(),
},
'&:active': {
"&:active": {
background: color(fgColor).alpha(0.15).toString(),
},
},
ButtonDisabled: {
filter: 'brightness(50%)',
cursor: 'initial',
filter: "brightness(50%)",
cursor: "initial",
},
ButtonError: {
border: `1px solid ${dangerColor}`,
color: dangerColor,
'&:hover': {
"&:hover": {
border: `1px solid ${dangerColor}`,
},
'&:active': {
"&:active": {
border: `1px solid ${dangerColor}`,
},
},
HoverError: {
display: 'none',
width: '100%',
position: 'absolute',
display: "none",
width: "100%",
position: "absolute",
},
HoverErrorContent: {
position: 'absolute',
transform: 'translateX(-50%)',
top: '-2.2em',
position: "absolute",
transform: "translateX(-50%)",
top: "-2.2em",
display: 'block',
display: "block",
background: bgColor,
},
LoadingMarker: {
position: 'absolute',
bottom: '0px',
left: '0px',
width: '3px',
height: '3px',
position: "absolute",
bottom: "0px",
left: "0px",
width: "3px",
height: "3px",
background: bgColor,
animation: '$loading-marker 3s ease infinite',
animation: "$loading-marker 3s ease infinite",
},
'@keyframes loading-marker': {
'0%, 100%': {
left: 'max(0%, min(30%, calc(50% - 50px)))',
"@keyframes loading-marker": {
"0%, 100%": {
left: "max(0%, min(30%, calc(50% - 50px)))",
},
'50%': {
left: 'min(calc(100% - 3px), max(70%, calc(50% + 50px)))',
"50%": {
left: "min(calc(100% - 3px), max(70%, calc(50% + 50px)))",
},
},
});
@@ -109,10 +109,10 @@ const Button = ({
children,
secondary,
errorStyle,
disabled,
disabled = false,
onPress = () => undefined,
...props
}: Omit<HTMLProps<HTMLDivElement>, 'className' | 'onClick'> & {
}: Omit<HTMLProps<HTMLDivElement>, "className" | "onClick"> & {
secondary?: boolean;
errorStyle?: boolean;
onPress?: (
@@ -172,7 +172,7 @@ const Button = ({
)}
>
{error ? (
<div {...classes('hover-error', sheet.classes.HoverError)}>
<div {...classes("hover-error", sheet.classes.HoverError)}>
<div className={sheet.classes.HoverErrorContent}>
<Button
onPress={(e) => {
@@ -184,8 +184,8 @@ const Button = ({
secondary
errorStyle
style={{
display: 'inline-block',
whiteSpace: 'nowrap',
display: "inline-block",
whiteSpace: "nowrap",
}}
>
{shortErrorString(error)}
@@ -194,7 +194,7 @@ const Button = ({
</div>
) : undefined}
{loading && (
<div {...classes('loading-marker', sheet.classes.LoadingMarker)} />
<div {...classes("loading-marker", sheet.classes.LoadingMarker)} />
)}
<div className="button-content">{children}</div>
</div>

View File

@@ -111,7 +111,8 @@ export default class DeterministicDeployer {
throw new Error("Missing details for deploying deployer contract");
}
const requiredBalance = BigInt(deployment.gasPrice) * BigInt(deployment.gasLimit);
const requiredBalance =
BigInt(deployment.gasPrice) * BigInt(deployment.gasLimit);
const currentBalance = await provider.getBalance(deployment.signerAddress);
const balanceDeficit = requiredBalance - currentBalance;
@@ -163,10 +164,15 @@ export default class DeterministicDeployer {
if (existingCode !== "0x") {
const { chainId } = await provider.getNetwork();
return new DeterministicDeployer(signer, chainId, {
signerAddress,
address,
}, overrides);
return new DeterministicDeployer(
signer,
chainId,
{
signerAddress,
address,
},
overrides,
);
}
const { chainId } = await provider.getNetwork();

View File

@@ -5,8 +5,9 @@ Please note, these plugins are in a pre-alpha state and are not ready for produc
# Getting Started
1. `cd packages/plugins`
2. Run `yarn` to install hardhat dependencies
3. Run `forge install` to install foundry dependencies
2. Run `yarn submodules` to initialize git submodules
3. Run `yarn` to install hardhat dependencies
4. Run `forge install` to install foundry dependencies
## (optional) ZKP Plugins

View File

@@ -1,7 +1,8 @@
{
"gasFactor": "1",
"port": "3000",
"entryPoint": "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789",
"network": "http://127.0.0.1:8545",
"entryPoint": "0x0000000071727De22E5E9d8BAf0edAc6f37da032",
"beneficiary": "0xd21934eD8eAf27a67f0A70042Af50A1D6d195E81",
"minBalance": "1",
"mnemonic": "./workdir/mnemonic.txt",

View File

@@ -4,6 +4,7 @@ out = "out"
libs = [
"lib",
]
solc_version = "0.8.23"
allow_paths = [
"../../primitives",

View File

@@ -16,7 +16,7 @@ function getRemappings() {
const config: HardhatUserConfig = {
solidity: {
version: "0.8.21",
version: "0.8.23",
settings: {
optimizer: {
enabled: true,
@@ -31,6 +31,12 @@ const config: HardhatUserConfig = {
gas: 100000000,
url: "http://localhost:8545",
},
basesepolia: {
url: "https://sepolia.base.org",
accounts: {
mnemonic: process.env.MNEMONIC,
},
},
},
mocha: {
timeout: 120000,
@@ -79,3 +85,18 @@ task("sendEth", "Sends ETH to an address")
await txnRes.wait();
},
);
task("generateMnemonic", "Generates and displays a random mnemonic").setAction(
async (_params, hre) => {
const wallet = hre.ethers.Wallet.createRandom();
console.log(wallet.mnemonic?.phrase);
},
);
task("accounts", "Prints the list of accounts", async (_params, hre) => {
const accounts = await hre.ethers.getSigners();
for (const account of accounts) {
console.log(account.address);
}
});

View File

@@ -1,11 +1,12 @@
{
"name": "@getwax/safe",
"name": "@getwax/plugins",
"private": true,
"version": "0.1.0",
"description": "Safe plugins for 4337 accounts",
"description": "Plugins for 4337 & SCA accounts",
"repository": "https://github.com/getwax/wax",
"license": "MIT",
"scripts": {
"submodules": "git submodule update --init --recursive",
"build": "hardhat compile",
"lint": "eslint . --ext js,jsx,ts,tsx --report-unused-disable-directives --max-warnings 0"
},
@@ -13,7 +14,7 @@
"@getwax/circuits": "../zkp"
},
"devDependencies": {
"@account-abstraction/contracts": "^0.6.0",
"@account-abstraction/contracts": "0.7.0",
"@account-abstraction/utils": "^0.6.0",
"@nomicfoundation/hardhat-chai-matchers": "^2.0.0",
"@nomicfoundation/hardhat-ethers": "^3.0.0",

View File

@@ -2,8 +2,14 @@ ds-test/=lib/forge-std/lib/ds-test/src/
forge-std/=lib/forge-std/src/
openzeppelin-contracts/=lib/openzeppelin-contracts/
@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/
account-abstraction/=lib/account-abstraction/
@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/
@eth-infinitism/account-abstraction/=lib/reference-implementation/lib/account-abstraction/contracts/
account-abstraction/=lib/account-abstraction/contracts/
safe-contracts/=lib/safe-contracts/
kernel/=lib/kernel/
I4337/=lib/kernel/lib/I4337/src/
solady/=lib/kernel/lib/solady/src/
solady/=lib/kernel/lib/solady/src/
erc7579-implementation/=lib/erc7579-implementation/
erc6900-reference-implementation/=lib/reference-implementation/src/
ether-email-auth/=lib/ether-email-auth/
@zk-email/contracts/=lib/zk-email-verify/packages/contracts/

View File

@@ -0,0 +1,36 @@
import hre from "hardhat";
import { SafeZkEmailRecoveryPlugin__factory } from "../typechain-types";
// base sepolia
// TODO make configurable
const emailAuthContracts = {
verifier: "0xEdC642bbaD91E21cCE6cd436Fdc6F040FD0fF998",
dkimRegistry: "0xC83256CCf7B94d310e49edA05077899ca036eb78",
emailAuthImpl: "0x1C76Aa365c17B40c7E944DcCdE4dC6e6D2A7b748",
};
async function deploySafeZkEmailRecoveryPlugin() {
console.log("Deploying SafeZkEmailRecoveryPlugin");
const [firstSigner] = await hre.ethers.getSigners();
console.log(`Using ${await firstSigner.getAddress()} as signer/deployer`);
const recoveryPlugin = await new SafeZkEmailRecoveryPlugin__factory()
.connect(firstSigner)
.deploy(
emailAuthContracts.verifier,
emailAuthContracts.dkimRegistry,
emailAuthContracts.emailAuthImpl,
);
await recoveryPlugin.waitForDeployment();
console.log(
`SafeZkEmailRecoveryPlugin deployed to ${await recoveryPlugin.getAddress()}`,
);
}
deploySafeZkEmailRecoveryPlugin().catch((error: Error) => {
console.error(error);
process.exitCode = 1;
});

View File

@@ -7,7 +7,6 @@ import DeterministicDeployer, {
import {
SimulateTxAccessor__factory,
SafeProxyFactory__factory,
TokenCallbackHandler__factory,
CompatibilityFallbackHandler__factory,
CreateCall__factory,
MultiSend__factory,
@@ -23,6 +22,8 @@ import {
AddressRegistry__factory,
} from "../typechain-types";
import makeDevFaster from "../test/e2e/utils/makeDevFaster";
import { TokenCallbackHandler__factory } from "../typechain-types/factories/lib/safe-contracts/contracts/handler/TokenCallbackHandler__factory";
import bundlerConfig from "./../config/bundler.config.json";
// 'test '.repeat(11) + 'absent'
const testAbsentAddress = "0xe8250207B79D7396631bb3aE38a7b457261ae0B6";

View File

@@ -1,16 +0,0 @@
FROM accountabstraction/bundler:0.6.2
RUN apt-get update && \
apt-get install -y coreutils && \
rm -rf /var/lib/apt/lists/*
ARG EXPECTED_DIGEST="727838bd8705dd319970fb4a86c9abe5334e17312230143e01043abf0732b5f7"
# Modify /app/bundler.js to add support for estimating (but not running) aggregate bundles
RUN sed -i 's/(errorResult.errorName !== '\''ValidationResult'\'')/(!errorResult.errorName.startsWith('\''ValidationResult'\''))/g' /app/bundler.js
# Fail if we didn't achieve the expected SHA256 digest
RUN if [ "$(sha256sum /app/bundler.js | awk '{print $1}')" != "$EXPECTED_DIGEST" ]; then \
echo "SHA256 digest does not match. Aborting."; \
exit 1; \
fi

View File

@@ -21,7 +21,7 @@
DOCKER_NETWORK=packages-plugins-docker-network
GETH_IMAGE=ethereum/client-go:v1.13.5
BUNDLER_IMAGE=patched-bundler-727838b
BUNDLER_IMAGE=accountabstraction/bundler:0.7.0
GETH_CONTAINER=geth${RANDOM}
BUNDLER_CONTAINER=bundler${RANDOM}
@@ -78,13 +78,9 @@ docker exec ${GETH_CONTAINER} geth \
# Deploy common contracts
yarn hardhat run "${SCRIPT_DIR}/deploy_all.ts" --network localhost
if ! docker images | grep -q "${BUNDLER_IMAGE}"; then
echo "Building '${BUNDLER_IMAGE}'..."
# Build the Docker image from the Dockerfile
docker build -f "${SCRIPT_DIR}/${BUNDLER_IMAGE}.dockerfile" -t "${BUNDLER_IMAGE}" .
fi
# Start ERC-4337 bundler
docker pull ${BUNDLER_IMAGE}
docker run --rm -i --name ${BUNDLER_CONTAINER} -p 3000:3000 -v "$PWD"/config:/app/workdir:ro --network=${DOCKER_NETWORK} ${BUNDLER_IMAGE} \
--network http://${GETH_CONTAINER}:8545 \
&

View File

@@ -0,0 +1,365 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.19;
import {BasePlugin} from "erc6900-reference-implementation/plugins/BasePlugin.sol";
import {IPluginExecutor} from "erc6900-reference-implementation/interfaces/IPluginExecutor.sol";
import {ManifestFunction, ManifestAssociatedFunctionType, ManifestAssociatedFunction, PluginManifest, PluginMetadata, IPlugin} from "erc6900-reference-implementation/interfaces/IPlugin.sol";
import {MockGroth16Verifier} from "../safe/utils/MockGroth16Verifier.sol";
import {MockDKIMRegsitry} from "../safe/utils/MockDKIMRegsitry.sol";
import {IDKIMRegsitry} from "../safe/interface/IDKIMRegsitry.sol";
/*//////////////////////////////////////////////////////////////////////////
THIS CONTRACT IS STILL IN ACTIVE DEVELOPMENT. NOT FOR PRODUCTION USE
//////////////////////////////////////////////////////////////////////////*/
struct RecoveryRequest {
bytes32 recoveryHash;
bytes32 dkimPublicKeyHash;
uint256 executeAfter;
address pendingNewOwner;
}
/// @title ZK Email Recovery Plugin
/// @author Wax
/// @notice This plugin recovers a ERC 6900 account via zk email guardians
contract ERC6900ZkEmailRecoveryModule is BasePlugin {
// metadata used by the pluginMetadata() method down below
string public constant NAME = "ZK Email Recovery Plugin";
string public constant VERSION = "1.0.0";
string public constant AUTHOR = "Wax";
// this is a constant used in the manifest, to reference our only dependency: the single owner plugin
// since it is the first, and only, plugin the index 0 will reference the single owner plugin
// we can use this to tell the modular account that we should use the single owner plugin to validate our user op
// in other words, we'll say "make sure the person calling the recovery functions is an owner of the account using our single plugin" // TODO: revisit this - recovery is more complicated as the owner is compromised
uint256
internal constant _MANIFEST_DEPENDENCY_INDEX_OWNER_USER_OP_VALIDATION =
0;
/** Default DKIM public key hashes registry */
IDKIMRegsitry public immutable defaultDkimRegistry;
/** verifier */
MockGroth16Verifier public immutable verifier;
/** Default delay has been set to a large timeframe on purpose. Please use a default delay suited to your specific context */
uint256 public constant defaultDelay = 2 weeks;
/** recovery hash domain */
bytes32 immutable RECOVERY_HASH_DOMAIN;
/** recovery request */
RecoveryRequest public recoveryRequest;
/** custom recovery delay */
uint256 public recoveryDelay;
/** dkim registry address */
address public dkimRegistry;
error RECOVERY_ALREADY_INITIATED();
error RECOVERY_NOT_CONFIGURED();
error INVALID_DKIM_KEY_HASH(
address account,
string emailDomain,
bytes32 dkimPublicKeyHash
);
error INVALID_PROOF();
error RECOVERY_NOT_INITIATED();
error DELAY_NOT_PASSED();
event RecoveryConfigured(
address indexed account,
address indexed owner,
bytes32 recoveryHash,
bytes32 dkimPublicKeyHash,
address dkimRegistry,
uint256 customDelay
);
event RecoveryInitiated(
address indexed account,
address newOwner,
uint256 executeAfter
);
event AccountRecovered(address indexed account, address newOwner);
event RecoveryCancelled(address indexed account);
event RecoveryDelaySet(address indexed account, uint256 delay);
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ Execution functions ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
/**
* @notice Initiates a recovery for an account using a zk email proof.
* @dev Rotates the account owner address to a new address. Uses the
* default delay period if no custom delay has been set. This is the second
* function that should be called in the recovery process - after configureRecovery
* @param newOwner The new owner address of the account
* @param emailDomain Domain name of the sender's email
* @param a Part of the proof
* @param b Part of the proof
* @param c Part of the proof
*/
function initiateRecovery(
address newOwner,
string memory emailDomain,
uint256[2] memory a,
uint256[2][2] memory b,
uint256[2] memory c
) external {
address account = msg.sender;
if (recoveryRequest.recoveryHash == bytes32(0)) {
revert RECOVERY_NOT_CONFIGURED();
}
if (recoveryRequest.executeAfter > 0) {
revert RECOVERY_ALREADY_INITIATED();
}
if (
!this.isDKIMPublicKeyHashValid(
emailDomain,
recoveryRequest.dkimPublicKeyHash
)
) {
revert INVALID_DKIM_KEY_HASH(
account,
emailDomain,
recoveryRequest.dkimPublicKeyHash
);
}
uint256[4] memory publicSignals = [
uint256(uint160(account)),
uint256(recoveryRequest.recoveryHash),
uint256(uint160(newOwner)),
uint256(recoveryRequest.dkimPublicKeyHash)
];
// verify proof
bool verified = verifier.verifyProof(a, b, c, publicSignals);
if (!verified) revert INVALID_PROOF();
uint256 executeAfter = block.timestamp + recoveryDelay;
recoveryRequest.executeAfter = executeAfter;
recoveryRequest.pendingNewOwner = newOwner;
emit RecoveryInitiated(account, newOwner, executeAfter);
}
/**
* @notice Recovers an account using a zk email proof.
* @dev Rotates the account owner address to a new address.
* This function is the third and final function that needs to be called in the
* recovery process. After configureRecovery & initiateRecovery
*/
function recoverAccount() public {
address account = msg.sender;
if (recoveryRequest.executeAfter == 0) {
revert RECOVERY_NOT_INITIATED();
}
if (block.timestamp > recoveryRequest.executeAfter) {
delete recoveryRequest;
// TODO: implement recovery logic for 6900 owner plugin
// owner = recoveryRequest.pendingNewOwner;
emit AccountRecovered(account, recoveryRequest.pendingNewOwner);
} else {
revert DELAY_NOT_PASSED();
}
}
/**
* @notice Cancels the recovery process of the sender if it exits.
* @dev Deletes the recovery request accociated with a account. Assumes
* the msg.sender is the account that the recovery request is being deleted for
*/
function cancelRecovery() external {
address account = msg.sender;
delete recoveryRequest;
emit RecoveryCancelled(account);
}
/**
* @notice Sets a custom delay for recovering the account.
* @dev Custom delay is used instead of the default delay when recovering the
* account. Custom delays should be configured with care as they can be
* used to bypass the default delay.
* @param delay The custom delay to be used when recovering the account
*/
function setRecoveryDelay(uint256 delay) external {
address account = msg.sender;
recoveryDelay = delay;
emit RecoveryDelaySet(account, delay);
}
/// @notice Return the DKIM public key hash for a given email domain and account address
/// @param emailDomain Email domain for which the DKIM public key hash is to be returned
function isDKIMPublicKeyHashValid(
string memory emailDomain,
bytes32 publicKeyHash
) public view returns (bool) {
if (dkimRegistry == address(0)) {
return
defaultDkimRegistry.isDKIMPublicKeyHashValid(
emailDomain,
publicKeyHash
);
} else {
return
IDKIMRegsitry(dkimRegistry).isDKIMPublicKeyHashValid(
emailDomain,
publicKeyHash
);
}
}
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ Plugin interface functions ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
/// @inheritdoc BasePlugin
function onInstall(bytes calldata data) external override {
(
bytes32 recoveryHash,
bytes32 dkimPublicKeyHash,
address _dkimRegistry,
uint256 customDelay
) = abi.decode(data, (bytes32, bytes32, address, uint256));
address account = msg.sender;
if (recoveryRequest.executeAfter > 0) {
revert RECOVERY_ALREADY_INITIATED();
}
if (customDelay > 0) {
recoveryDelay = customDelay;
} else {
recoveryDelay = defaultDelay;
}
recoveryRequest = RecoveryRequest({
recoveryHash: recoveryHash,
dkimPublicKeyHash: dkimPublicKeyHash,
executeAfter: 0,
pendingNewOwner: address(0)
});
dkimRegistry = _dkimRegistry; // FIXME: could be zero
emit RecoveryConfigured(
account,
msg.sender,
recoveryHash,
dkimPublicKeyHash,
dkimRegistry,
customDelay
);
}
/// @inheritdoc BasePlugin
function onUninstall(bytes calldata) external pure override {}
/// @inheritdoc BasePlugin
function pluginManifest()
external
pure
override
returns (PluginManifest memory)
{
PluginManifest memory manifest;
// since we are using the modular account, we will specify one depedency
// which will handle the user op validation for ownership
// you can find this depedency specified in the installPlugin call in the tests
manifest.dependencyInterfaceIds = new bytes4[](1);
manifest.dependencyInterfaceIds[0] = type(IPlugin).interfaceId;
// we have several execution functions that can be called, here we define
// those functions on the manifest as something that can be called during execution
manifest.executionFunctions = new bytes4[](5);
manifest.executionFunctions[0] = this.initiateRecovery.selector;
manifest.executionFunctions[1] = this.recoverAccount.selector;
manifest.executionFunctions[2] = this.cancelRecovery.selector;
manifest.executionFunctions[3] = this.setRecoveryDelay.selector;
manifest.executionFunctions[4] = this.isDKIMPublicKeyHashValid.selector;
// you can think of ManifestFunction as a reference to a function somewhere,
// we want to say "use this function" for some purpose - in this case,
// we'll be using the user op validation function from the single owner dependency
// and this is specified by the depdendency index
ManifestFunction
memory ownerUserOpValidationFunction = ManifestFunction({
functionType: ManifestAssociatedFunctionType.DEPENDENCY,
functionId: 0, // unused since it's a dependency
dependencyIndex: _MANIFEST_DEPENDENCY_INDEX_OWNER_USER_OP_VALIDATION
});
// here we will link together the recovery functions with the single owner user op validation
// this basically says "use this user op validation function and make sure everythings okay before calling the recovery functions"
// this will ensure that only an owner of the account can call the recovery functions
manifest.userOpValidationFunctions = new ManifestAssociatedFunction[](
1
);
manifest.userOpValidationFunctions[0] = ManifestAssociatedFunction({
executionSelector: this.initiateRecovery.selector,
associatedFunction: ownerUserOpValidationFunction
});
// TODO: recoverAccount should be permissionless if threshold is met
manifest.userOpValidationFunctions[1] = ManifestAssociatedFunction({
executionSelector: this.recoverAccount.selector,
associatedFunction: ownerUserOpValidationFunction
});
manifest.userOpValidationFunctions[2] = ManifestAssociatedFunction({
executionSelector: this.cancelRecovery.selector,
associatedFunction: ownerUserOpValidationFunction
});
manifest.userOpValidationFunctions[3] = ManifestAssociatedFunction({
executionSelector: this.setRecoveryDelay.selector,
associatedFunction: ownerUserOpValidationFunction
});
manifest.userOpValidationFunctions[4] = ManifestAssociatedFunction({
executionSelector: this.isDKIMPublicKeyHashValid.selector,
associatedFunction: ownerUserOpValidationFunction
});
// TODO: research best way to utilise this part of the manifest
// finally here we will always deny runtime calls to the initiateRecovery function as we will only call it through user ops
// this avoids a potential issue where a future plugin may define
// a runtime validation function for it and unauthorized calls may occur due to that
manifest.preRuntimeValidationHooks = new ManifestAssociatedFunction[](
1
);
manifest.preRuntimeValidationHooks[0] = ManifestAssociatedFunction({
executionSelector: this.initiateRecovery.selector,
associatedFunction: ManifestFunction({
functionType: ManifestAssociatedFunctionType
.PRE_HOOK_ALWAYS_DENY,
functionId: 0,
dependencyIndex: 0
})
});
return manifest;
}
/// @inheritdoc BasePlugin
function pluginMetadata()
external
pure
virtual
override
returns (PluginMetadata memory)
{
PluginMetadata memory metadata;
metadata.name = NAME;
metadata.version = VERSION;
metadata.author = AUTHOR;
return metadata;
}
}

View File

@@ -0,0 +1,275 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
/* solhint-disable avoid-low-level-calls */
/* solhint-disable no-inline-assembly */
/* solhint-disable reason-string */
import {SimpleAccount} from "account-abstraction/samples/SimpleAccount.sol";
import {IEntryPoint} from "account-abstraction/interfaces/IEntryPoint.sol";
import {MockGroth16Verifier} from "./safe/utils/MockGroth16Verifier.sol";
import {MockDKIMRegsitry} from "./safe/utils/MockDKIMRegsitry.sol";
import {IDKIMRegsitry} from "./safe/interface/IDKIMRegsitry.sol";
/*//////////////////////////////////////////////////////////////////////////
THIS CONTRACT IS STILL IN ACTIVE DEVELOPMENT. NOT FOR PRODUCTION USE
//////////////////////////////////////////////////////////////////////////*/
struct RecoveryRequest {
bytes32 recoveryHash;
bytes32 dkimPublicKeyHash;
uint256 executeAfter;
address pendingNewOwner;
}
/**
* minimal account.
* this is sample minimal account with ZK Email recovery for rotating the owner
*/
contract SimpleAccountWithRecovery is SimpleAccount {
/** Default DKIM public key hashes registry */
IDKIMRegsitry public immutable defaultDkimRegistry;
/** verifier */
MockGroth16Verifier public immutable verifier;
/** Default delay has been set to a large timeframe on purpose. Please use a default delay suited to your specific context */
uint256 public constant defaultDelay = 2 weeks;
/** recovery hash domain */
bytes32 immutable RECOVERY_HASH_DOMAIN;
/** recovery request */
RecoveryRequest public recoveryRequest;
/** custom recovery delay */
uint256 public recoveryDelay;
/** dkim registry address */
address public dkimRegistry;
error RECOVERY_ALREADY_INITIATED();
error RECOVERY_NOT_CONFIGURED();
error INVALID_DKIM_KEY_HASH(
address account,
string emailDomain,
bytes32 dkimPublicKeyHash
);
error INVALID_PROOF();
error RECOVERY_NOT_INITIATED();
error DELAY_NOT_PASSED();
event RecoveryConfigured(
address indexed account,
address indexed owner,
bytes32 recoveryHash,
bytes32 dkimPublicKeyHash,
address dkimRegistry,
uint256 customDelay
);
event RecoveryInitiated(
address indexed account,
address newOwner,
uint256 executeAfter
);
event AccountRecovered(address indexed account, address newOwner);
event RecoveryCancelled(address indexed account);
event RecoveryDelaySet(address indexed account, uint256 delay);
constructor(
IEntryPoint anEntryPoint,
address _verifier,
address _defaultDkimRegistry
) SimpleAccount(anEntryPoint) {
verifier = MockGroth16Verifier(_verifier);
defaultDkimRegistry = IDKIMRegsitry(_defaultDkimRegistry);
RECOVERY_HASH_DOMAIN = keccak256(
abi.encode(
keccak256(
"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"
),
keccak256("SimpleAccountWithRecovery"),
keccak256("1"),
block.chainid,
address(this)
)
);
}
/**
* @notice Stores a recovery hash that can be used to recover the account
* @dev dkimRegistry can be a zero address if the user wants to use the
* defaultDkimRegistry. customDelay can be 0 if the user wants to use defaultDelay
* This is the first function that must be called when setting up recovery.
* @param recoveryHash Hash of domain, email and salt - keccak256(abi.encodePacked(RECOVERY_HASH_DOMAIN, email, salt))
* @param dkimPublicKeyHash Hash of DKIM public key - keccak256(abi.encodePacked(dkimPublicKey))
* @param _dkimRegistry Address of a user-defined DKIM registry
* @param customDelay A custom delay to set the recoveryDelay value that is associated with a account.
*/
function configureRecovery(
bytes32 recoveryHash,
bytes32 dkimPublicKeyHash,
address _dkimRegistry,
uint256 customDelay
) external onlyOwner {
address account = address(this);
if (recoveryRequest.executeAfter > 0) {
revert RECOVERY_ALREADY_INITIATED();
}
if (customDelay > 0) {
recoveryDelay = customDelay;
} else {
recoveryDelay = defaultDelay;
}
recoveryRequest = RecoveryRequest({
recoveryHash: recoveryHash,
dkimPublicKeyHash: dkimPublicKeyHash,
executeAfter: 0,
pendingNewOwner: address(0)
});
dkimRegistry = _dkimRegistry; // FIXME: could be zero
emit RecoveryConfigured(
account,
owner,
recoveryHash,
dkimPublicKeyHash,
dkimRegistry,
customDelay
);
}
/**
* @notice Initiates a recovery for an account using a zk email proof.
* @dev Rotates the account owner address to a new address. Uses the
* default delay period if no custom delay has been set. This is the second
* function that should be called in the recovery process - after configureRecovery
* @param newOwner The new owner address of the account
* @param emailDomain Domain name of the sender's email
* @param a Part of the proof
* @param b Part of the proof
* @param c Part of the proof
*/
function initiateRecovery(
address newOwner,
string memory emailDomain,
uint256[2] memory a,
uint256[2][2] memory b,
uint256[2] memory c
) external {
address account = address(this);
if (recoveryRequest.recoveryHash == bytes32(0)) {
revert RECOVERY_NOT_CONFIGURED();
}
if (recoveryRequest.executeAfter > 0) {
revert RECOVERY_ALREADY_INITIATED();
}
if (
!this.isDKIMPublicKeyHashValid(
emailDomain,
recoveryRequest.dkimPublicKeyHash
)
) {
revert INVALID_DKIM_KEY_HASH(
account,
emailDomain,
recoveryRequest.dkimPublicKeyHash
);
}
uint256[4] memory publicSignals = [
uint256(uint160(account)),
uint256(recoveryRequest.recoveryHash),
uint256(uint160(newOwner)),
uint256(recoveryRequest.dkimPublicKeyHash)
];
// verify proof
bool verified = verifier.verifyProof(a, b, c, publicSignals);
if (!verified) revert INVALID_PROOF();
uint256 executeAfter = block.timestamp + recoveryDelay;
recoveryRequest.executeAfter = executeAfter;
recoveryRequest.pendingNewOwner = newOwner;
emit RecoveryInitiated(account, newOwner, executeAfter);
}
/**
* @notice Recovers an account using a zk email proof.
* @dev Rotates the account owner address to a new address.
* This function is the third and final function that needs to be called in the
* recovery process. After configureRecovery & initiateRecovery
*/
function recoverAccount() public {
address account = address(this);
if (recoveryRequest.executeAfter == 0) {
revert RECOVERY_NOT_INITIATED();
}
if (block.timestamp > recoveryRequest.executeAfter) {
delete recoveryRequest;
owner = recoveryRequest.pendingNewOwner;
emit AccountRecovered(account, recoveryRequest.pendingNewOwner);
} else {
revert DELAY_NOT_PASSED();
}
}
/**
* @notice Cancels the recovery process of the sender if it exits.
* @dev Deletes the recovery request accociated with a account. Assumes
* the msg.sender is the account that the recovery request is being deleted for
*/
function cancelRecovery() external {
address account = address(this);
delete recoveryRequest;
emit RecoveryCancelled(account);
}
/**
* @notice Sets a custom delay for recovering the account.
* @dev Custom delay is used instead of the default delay when recovering the
* account. Custom delays should be configured with care as they can be
* used to bypass the default delay.
* @param delay The custom delay to be used when recovering the account
*/
function setRecoveryDelay(uint256 delay) external {
address account = address(this);
recoveryDelay = delay;
emit RecoveryDelaySet(account, delay);
}
/// @notice Return the DKIM public key hash for a given email domain and account address
/// @param emailDomain Email domain for which the DKIM public key hash is to be returned
function isDKIMPublicKeyHashValid(
string memory emailDomain,
bytes32 publicKeyHash
) public view returns (bool) {
if (dkimRegistry == address(0)) {
return
defaultDkimRegistry.isDKIMPublicKeyHashValid(
emailDomain,
publicKeyHash
);
} else {
return
IDKIMRegsitry(dkimRegistry).isDKIMPublicKeyHashValid(
emailDomain,
publicKeyHash
);
}
}
}

View File

@@ -0,0 +1,276 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import {IValidator, VALIDATION_SUCCESS, VALIDATION_FAILED, MODULE_TYPE_VALIDATOR} from "erc7579-implementation/src/interfaces/IERC7579Module.sol";
import {ModeLib, ModeCode, CallType, CALLTYPE_SINGLE} from "erc7579-implementation/src/lib/ModeLib.sol";
import {IERC7579Account} from "erc7579-implementation/src/interfaces/IERC7579Account.sol";
import {ExecutionLib} from "erc7579-implementation/src/lib/ExecutionLib.sol";
import {PackedUserOperation} from "account-abstraction/interfaces/IEntryPoint.sol";
import {ValidationData} from "account-abstraction/core/Helpers.sol";
import {MockGroth16Verifier} from "../safe/utils/MockGroth16Verifier.sol";
import {MockDKIMRegsitry} from "../safe/utils/MockDKIMRegsitry.sol";
import {IDKIMRegsitry} from "../safe/interface/IDKIMRegsitry.sol";
/*//////////////////////////////////////////////////////////////////////////
THIS CONTRACT IS STILL IN ACTIVE DEVELOPMENT. NOT FOR PRODUCTION USE
//////////////////////////////////////////////////////////////////////////*/
struct RecoveryRequest {
bytes32 recoveryHash;
bytes32 dkimPublicKeyHash;
uint256 executeAfter;
address pendingNewOwner;
}
contract ERC7579ZkEmailRecoveryModule is IValidator {
/*//////////////////////////////////////////////////////////////////////////
CONSTANTS & STORAGE
//////////////////////////////////////////////////////////////////////////*/
/** Default DKIM public key hashes registry */
IDKIMRegsitry public immutable defaultDkimRegistry;
/** verifier */
MockGroth16Verifier public immutable verifier;
/** Default delay has been set to a large timeframe on purpose. Please use a default
delay suited to your specific context */
uint256 public constant defaultDelay = 2 weeks;
/** recovery hash domain */
bytes32 immutable RECOVERY_HASH_DOMAIN;
/** recovery request */
RecoveryRequest public recoveryRequest;
/** custom recovery delay */
uint256 public recoveryDelay;
/** dkim registry address */
address public dkimRegistry;
mapping(address => bool) internal initialized;
error RECOVERY_ALREADY_INITIATED();
error RECOVERY_NOT_CONFIGURED();
error INVALID_DKIM_KEY_HASH(
address account,
string emailDomain,
bytes32 dkimPublicKeyHash
);
error INVALID_PROOF();
error RECOVERY_NOT_INITIATED();
error DELAY_NOT_PASSED();
error UNSUPPORTED_OPERATION();
event RecoveryConfigured(
address indexed account,
address indexed owner,
bytes32 recoveryHash,
bytes32 dkimPublicKeyHash,
address dkimRegistry,
uint256 customDelay
);
event RecoveryInitiated(
address indexed account,
address newOwner,
uint256 executeAfter
);
event AccountRecovered(address indexed account, address newOwner);
event RecoveryCancelled(address indexed account);
event RecoveryDelaySet(address indexed account, uint256 delay);
/*//////////////////////////////////////////////////////////////////////////
CONFIG
//////////////////////////////////////////////////////////////////////////*/
function onInstall(bytes calldata data) external override {
if (isInitialized(msg.sender)) revert AlreadyInitialized(msg.sender);
(
bytes32 recoveryHash,
bytes32 dkimPublicKeyHash,
address _dkimRegistry,
uint256 customDelay
) = abi.decode(data, (bytes32, bytes32, address, uint256));
address account = msg.sender;
if (recoveryRequest.executeAfter > 0) {
revert RECOVERY_ALREADY_INITIATED();
}
if (customDelay > 0) {
recoveryDelay = customDelay;
} else {
recoveryDelay = defaultDelay;
}
recoveryRequest = RecoveryRequest({
recoveryHash: recoveryHash,
dkimPublicKeyHash: dkimPublicKeyHash,
executeAfter: 0,
pendingNewOwner: address(0)
});
dkimRegistry = _dkimRegistry; // FIXME: could be zero
initialized[msg.sender] = true;
emit RecoveryConfigured(
account,
msg.sender,
recoveryHash,
dkimPublicKeyHash,
dkimRegistry,
customDelay
);
}
function onUninstall(bytes calldata) external override {
if (!isInitialized(msg.sender)) revert NotInitialized(msg.sender);
initialized[msg.sender] = false;
}
function isInitialized(address smartAccount) public view returns (bool) {
return initialized[smartAccount];
}
/*//////////////////////////////////////////////////////////////////////////
MODULE LOGIC
//////////////////////////////////////////////////////////////////////////*/
function validateUserOp(
PackedUserOperation calldata userOp,
bytes32 userOpHash
) external override returns (uint256) {
address account = address(this);
(
address newOwner,
string memory emailDomain,
uint256[2] memory a,
uint256[2][2] memory b,
uint256[2] memory c
) = abi.decode(
userOp.signature,
(address, string, uint256[2], uint256[2][2], uint256[2])
);
// Check if the execution is allowed
bool isAllowedExecution;
bytes4 selector = bytes4(userOp.callData[0:4]);
if (selector == IERC7579Account.execute.selector) {
// Decode and check the execution
// Only single executions to installed validators are allowed
isAllowedExecution = _decodeAndCheckExecution(userOp.callData);
}
if (recoveryRequest.recoveryHash == bytes32(0)) {
return VALIDATION_FAILED;
}
if (recoveryRequest.executeAfter > 0) {
return VALIDATION_FAILED;
}
if (
!this.isDKIMPublicKeyHashValid(
emailDomain,
recoveryRequest.dkimPublicKeyHash
)
) {
return VALIDATION_FAILED;
}
uint256[4] memory publicSignals = [
uint256(uint160(account)),
uint256(recoveryRequest.recoveryHash),
uint256(uint160(newOwner)),
uint256(recoveryRequest.dkimPublicKeyHash)
];
// verify proof
bool verified = verifier.verifyProof(a, b, c, publicSignals);
if (!verified) return VALIDATION_FAILED;
uint256 executeAfter = block.timestamp + recoveryDelay;
recoveryRequest.executeAfter = executeAfter;
recoveryRequest.pendingNewOwner = newOwner;
emit RecoveryInitiated(account, newOwner, executeAfter);
return VALIDATION_SUCCESS;
}
function isValidSignatureWithSender(
address,
bytes32 hash,
bytes calldata data
) external view override returns (bytes4) {
// ERC-1271 not supported for recovery
revert UNSUPPORTED_OPERATION();
}
/// @notice Return the DKIM public key hash for a given email domain and account address
/// @param emailDomain Email domain for which the DKIM public key hash is to be returned
function isDKIMPublicKeyHashValid(
string memory emailDomain,
bytes32 publicKeyHash
) public view returns (bool) {
if (dkimRegistry == address(0)) {
return
defaultDkimRegistry.isDKIMPublicKeyHashValid(
emailDomain,
publicKeyHash
);
} else {
return
IDKIMRegsitry(dkimRegistry).isDKIMPublicKeyHashValid(
emailDomain,
publicKeyHash
);
}
}
/*//////////////////////////////////////////////////////////////////////////
INTERNAL
//////////////////////////////////////////////////////////////////////////*/
function _decodeAndCheckExecution(
bytes calldata callData
) internal returns (bool isAllowedExecution) {
// Get the mode and call type
ModeCode mode = ModeCode.wrap(bytes32(callData[4:36]));
CallType calltype = ModeLib.getCallType(mode);
if (calltype == CALLTYPE_SINGLE) {
// Decode the calldata
(address to, , ) = ExecutionLib.decodeSingle(callData[100:]);
// Check if the module is installed as a validator
return
IERC7579Account(msg.sender).isModuleInstalled(
MODULE_TYPE_VALIDATOR,
to,
""
);
} else {
return false;
}
}
/*//////////////////////////////////////////////////////////////////////////
METADATA
//////////////////////////////////////////////////////////////////////////*/
function name() external pure returns (string memory) {
return "SocialRecoveryValidator";
}
function version() external pure returns (string memory) {
return "0.0.1";
}
function isModuleType(uint256 typeID) external view returns (bool) {
return typeID == MODULE_TYPE_VALIDATOR;
}
}

View File

@@ -5,7 +5,11 @@ pragma solidity >=0.8.4 <0.9.0;
import {IKernelValidator, UserOperation} from "kernel/src/interfaces/IKernelValidator.sol";
import {ValidationData} from "kernel/src/common/Types.sol";
import {SIG_VALIDATION_FAILED, ValidationData} from "kernel/src/common/Constants.sol";
import {BLS} from "account-abstraction/contracts/samples/bls/lib/hubble-contracts/contracts/libs/BLS.sol";
import {BLS} from "account-abstraction/samples/bls/lib/hubble-contracts/contracts/libs/BLS.sol";
/*//////////////////////////////////////////////////////////////////////////
THIS CONTRACT IS STILL IN ACTIVE DEVELOPMENT. NOT FOR PRODUCTION USE
//////////////////////////////////////////////////////////////////////////*/
// BLSValidator is a validator that uses BLS signatures to validate transactions.
// TODO: Consider account recovery, aggregate signatures, and use EIP 712.

View File

@@ -3,6 +3,10 @@ pragma solidity ^0.8.12;
import {FCL_WebAuthn} from "./libraries/FCL_Webauthn.sol";
/*//////////////////////////////////////////////////////////////////////////
THIS CONTRACT IS STILL IN ACTIVE DEVELOPMENT. NOT FOR PRODUCTION USE
//////////////////////////////////////////////////////////////////////////*/
contract WebAuthn {
function verifySignature(
bytes calldata authenticatorData,
@@ -13,15 +17,14 @@ contract WebAuthn {
uint256[2] calldata signature,
uint256[2] calldata publicKey
) internal returns (bool verified) {
verified =
FCL_WebAuthn.checkSignature(
authenticatorData,
authenticatorDataFlagMask,
clientData,
clientChallenge,
clientChallengeDataOffset,
signature,
publicKey
);
verified = FCL_WebAuthn.checkSignature(
authenticatorData,
authenticatorDataFlagMask,
clientData,
clientChallenge,
clientChallengeDataOffset,
signature,
publicKey
);
}
}

View File

@@ -0,0 +1,138 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import {EmailAuthMsg} from "ether-email-auth/packages/contracts/src/EmailAuth.sol";
// import "forge-std/console.sol";
interface IEmailAccountRecovery {
function verifier() external view returns (address);
function dkim() external view returns (address);
function emailAuthImplementation() external view returns (address);
function acceptanceSubjectTemplates()
external
view
returns (string[][] memory);
function recoverySubjectTemplates()
external
view
returns (string[][] memory);
function computeEmailAuthAddress(
bytes32 accountSalt
) external view returns (address);
function computeAcceptanceTemplateId(
uint templateIdx
) external view returns (uint);
function computeRecoveryTemplateId(
uint templateIdx
) external view returns (uint);
function handleAcceptance(
EmailAuthMsg memory emailAuthMsg,
uint templateIdx
) external;
function handleRecovery(
EmailAuthMsg memory emailAuthMsg,
uint templateIdx
) external;
function completeRecovery() external;
}
/** Helper contract that routes relayer calls to correct EmailAccountRecovery implementation */
contract EmailAccountRecoveryRouter {
address public immutable emailAccountRecoveryImpl;
constructor(address _emailAccountRecoveryImpl) {
emailAccountRecoveryImpl = _emailAccountRecoveryImpl;
}
function verifier() external view returns (address) {
return IEmailAccountRecovery(emailAccountRecoveryImpl).verifier();
}
function dkim() external view returns (address) {
return IEmailAccountRecovery(emailAccountRecoveryImpl).dkim();
}
function emailAuthImplementation() external view returns (address) {
return
IEmailAccountRecovery(emailAccountRecoveryImpl)
.emailAuthImplementation();
}
function acceptanceSubjectTemplates()
external
view
returns (string[][] memory)
{
return
IEmailAccountRecovery(emailAccountRecoveryImpl)
.acceptanceSubjectTemplates();
}
function recoverySubjectTemplates()
external
view
returns (string[][] memory)
{
return
IEmailAccountRecovery(emailAccountRecoveryImpl)
.recoverySubjectTemplates();
}
function computeEmailAuthAddress(
bytes32 accountSalt
) external view returns (address) {
return
IEmailAccountRecovery(emailAccountRecoveryImpl)
.computeEmailAuthAddress(accountSalt);
}
function computeAcceptanceTemplateId(
uint templateIdx
) external view returns (uint) {
return
IEmailAccountRecovery(emailAccountRecoveryImpl)
.computeAcceptanceTemplateId(templateIdx);
}
function computeRecoveryTemplateId(
uint templateIdx
) external view returns (uint) {
return
IEmailAccountRecovery(emailAccountRecoveryImpl)
.computeRecoveryTemplateId(templateIdx);
}
function handleAcceptance(
EmailAuthMsg memory emailAuthMsg,
uint templateIdx
) external {
IEmailAccountRecovery(emailAccountRecoveryImpl).handleAcceptance(
emailAuthMsg,
templateIdx
);
}
function handleRecovery(
EmailAuthMsg memory emailAuthMsg,
uint templateIdx
) external {
IEmailAccountRecovery(emailAccountRecoveryImpl).handleRecovery(
emailAuthMsg,
templateIdx
);
}
function completeRecovery() external {
IEmailAccountRecovery(emailAccountRecoveryImpl).completeRecovery();
}
}

View File

@@ -4,12 +4,16 @@ pragma abicoder v2;
import {HandlerContext} from "safe-contracts/contracts/handler/HandlerContext.sol";
import {IEntryPoint, UserOperation} from "account-abstraction/contracts/interfaces/IEntryPoint.sol";
import {BLS} from "account-abstraction/contracts/samples/bls/lib/hubble-contracts/contracts/libs/BLS.sol";
import {IBLSAccount} from "account-abstraction/contracts/samples/bls/IBLSAccount.sol";
import {IEntryPoint, PackedUserOperation} from "account-abstraction/interfaces/IEntryPoint.sol";
import {BLS} from "account-abstraction/samples/bls/lib/hubble-contracts/contracts/libs/BLS.sol";
import {IBLSAccount} from "account-abstraction/samples/bls/IBLSAccount.sol";
import {Safe4337Base, ISafe} from "./utils/Safe4337Base.sol";
/*//////////////////////////////////////////////////////////////////////////
THIS CONTRACT IS STILL IN ACTIVE DEVELOPMENT. NOT FOR PRODUCTION USE
//////////////////////////////////////////////////////////////////////////*/
error IncorrectSignatureLength(uint256 length);
contract SafeBlsPlugin is Safe4337Base, IBLSAccount {
@@ -59,14 +63,15 @@ contract SafeBlsPlugin is Safe4337Base, IBLSAccount {
}
function _validateSignature(
UserOperation calldata userOp,
PackedUserOperation calldata userOp,
bytes32 /* userOpHash */
) internal view override returns (uint256) {
uint256 initCodeLen = userOp.initCode.length;
if (initCodeLen > 0) {
bytes32 claimedKeyHash =
keccak256(userOp.initCode[initCodeLen - 128:]);
bytes32 claimedKeyHash = keccak256(
userOp.initCode[initCodeLen - 128:]
);
// See appendKeyToInitCode.ts for a detailed explanation.
require(

View File

@@ -6,11 +6,15 @@ import {Safe} from "safe-contracts/contracts/Safe.sol";
import {SafeProxyFactory} from "safe-contracts/contracts/proxies/SafeProxyFactory.sol";
import {SafeProxy} from "safe-contracts/contracts/proxies/SafeProxy.sol";
import {EntryPoint} from "account-abstraction/contracts/core/EntryPoint.sol";
import {EntryPoint} from "account-abstraction/core/EntryPoint.sol";
import {SafeCompressionPlugin} from "./SafeCompressionPlugin.sol";
import {IDecompressor} from "../compression/decompressors/IDecompressor.sol";
/*//////////////////////////////////////////////////////////////////////////
THIS CONTRACT IS STILL IN ACTIVE DEVELOPMENT. NOT FOR PRODUCTION USE
//////////////////////////////////////////////////////////////////////////*/
contract SafeCompressionFactory {
function create(
Safe safeSingleton,
@@ -23,9 +27,9 @@ contract SafeCompressionFactory {
) external returns (SafeCompressionPlugin) {
bytes32 salt = keccak256(abi.encodePacked(owner, saltNonce));
Safe safe = Safe(payable(new SafeProxy{salt: salt}(
address(safeSingleton)
)));
Safe safe = Safe(
payable(new SafeProxy{salt: salt}(address(safeSingleton)))
);
address[] memory owners = new address[](1);
owners[0] = owner;

View File

@@ -4,14 +4,19 @@ pragma abicoder v2;
import {HandlerContext} from "safe-contracts/contracts/handler/HandlerContext.sol";
import {IEntryPoint, UserOperation} from "account-abstraction/contracts/interfaces/IEntryPoint.sol";
import {BLS} from "account-abstraction/contracts/samples/bls/lib/hubble-contracts/contracts/libs/BLS.sol";
import {IBLSAccount} from "account-abstraction/contracts/samples/bls/IBLSAccount.sol";
import {IEntryPoint, PackedUserOperation} from "account-abstraction/interfaces/IEntryPoint.sol";
import {BLS} from "account-abstraction/samples/bls/lib/hubble-contracts/contracts/libs/BLS.sol";
import {IBLSAccount} from "account-abstraction/samples/bls/IBLSAccount.sol";
import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol";
import {Safe4337Base, ISafe} from "./utils/Safe4337Base.sol";
import {Safe4337Base, ISafe, SIG_VALIDATION_FAILED} from "./utils/Safe4337Base.sol";
import {WaxLib as W} from "../compression/WaxLib.sol";
import {IDecompressor} from "../compression/decompressors/IDecompressor.sol";
/*//////////////////////////////////////////////////////////////////////////
THIS CONTRACT IS STILL IN ACTIVE DEVELOPMENT. NOT FOR PRODUCTION USE
//////////////////////////////////////////////////////////////////////////*/
error IncorrectSignatureLength(uint256 length);
contract SafeCompressionPlugin is Safe4337Base, IBLSAccount {
@@ -38,12 +43,10 @@ contract SafeCompressionPlugin is Safe4337Base, IBLSAccount {
_decompressor = decompressorParam;
}
function decompressAndPerform(
bytes calldata stream
) public {
function decompressAndPerform(bytes calldata stream) public {
_requireFromEntryPoint();
(W.Action[] memory actions,) = _decompressor.decompress(stream);
(W.Action[] memory actions, ) = _decompressor.decompress(stream);
ISafe safe = _currentSafe();
@@ -57,9 +60,7 @@ contract SafeCompressionPlugin is Safe4337Base, IBLSAccount {
}
}
function setDecompressor(
IDecompressor decompressorParam
) public {
function setDecompressor(IDecompressor decompressorParam) public {
_requireFromCurrentSafeOrEntryPoint();
_decompressor = decompressorParam;
}
@@ -77,14 +78,15 @@ contract SafeCompressionPlugin is Safe4337Base, IBLSAccount {
}
function _validateSignature(
UserOperation calldata userOp,
PackedUserOperation calldata userOp,
bytes32 /* userOpHash */
) internal view override returns (uint256) {
uint256 initCodeLen = userOp.initCode.length;
if (initCodeLen > 0) {
bytes32 claimedKeyHash =
keccak256(userOp.initCode[initCodeLen - 128:]);
bytes32 claimedKeyHash = keccak256(
userOp.initCode[initCodeLen - 128:]
);
// See appendKeyToInitCode.ts for a detailed explanation.
require(

View File

@@ -6,10 +6,14 @@ import {Safe} from "safe-contracts/contracts/Safe.sol";
import {SafeProxyFactory} from "safe-contracts/contracts/proxies/SafeProxyFactory.sol";
import {SafeProxy} from "safe-contracts/contracts/proxies/SafeProxy.sol";
import {EntryPoint} from "account-abstraction/contracts/core/EntryPoint.sol";
import {EntryPoint} from "account-abstraction/core/EntryPoint.sol";
import {SafeECDSAPlugin} from "./SafeECDSAPlugin.sol";
/*//////////////////////////////////////////////////////////////////////////
THIS CONTRACT IS STILL IN ACTIVE DEVELOPMENT. NOT FOR PRODUCTION USE
//////////////////////////////////////////////////////////////////////////*/
contract SafeECDSAFactory {
function create(
Safe safeSingleton,
@@ -19,9 +23,9 @@ contract SafeECDSAFactory {
) external returns (SafeECDSAPlugin) {
bytes32 salt = keccak256(abi.encodePacked(owner, saltNonce));
Safe safe = Safe(payable(new SafeProxy{salt: salt}(
address(safeSingleton)
)));
Safe safe = Safe(
payable(new SafeProxy{salt: salt}(address(safeSingleton)))
);
address[] memory owners = new address[](1);
owners[0] = owner;

View File

@@ -2,11 +2,16 @@
pragma solidity >=0.7.0 <0.9.0;
pragma abicoder v2;
import {Safe4337Base} from "./utils/Safe4337Base.sol";
import {IEntryPoint, UserOperation} from "account-abstraction/contracts/interfaces/IEntryPoint.sol";
import {UserOperation} from "account-abstraction/contracts/interfaces/IEntryPoint.sol";
import {Safe4337Base, SIG_VALIDATION_FAILED} from "./utils/Safe4337Base.sol";
import {IEntryPoint, PackedUserOperation} from "account-abstraction/interfaces/IEntryPoint.sol";
import {PackedUserOperation} from "account-abstraction/interfaces/IEntryPoint.sol";
import {ECDSA} from "openzeppelin-contracts/contracts/utils/cryptography/ECDSA.sol";
import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol";
/*//////////////////////////////////////////////////////////////////////////
THIS CONTRACT IS STILL IN ACTIVE DEVELOPMENT. NOT FOR PRODUCTION USE
//////////////////////////////////////////////////////////////////////////*/
interface ISafe {
function enableModule(address module) external;
@@ -88,11 +93,11 @@ contract SafeECDSAPlugin is Safe4337Base {
}
function _validateSignature(
UserOperation calldata userOp,
PackedUserOperation calldata userOp,
bytes32 userOpHash
) internal view override returns (uint256 validationData) {
address keyOwner = ecdsaOwnerStorage[msg.sender].owner;
bytes32 hash = userOpHash.toEthSignedMessageHash();
bytes32 hash = MessageHashUtils.toEthSignedMessageHash(userOpHash);
if (keyOwner != hash.recover(userOp.signature)) {
return SIG_VALIDATION_FAILED;

View File

@@ -2,6 +2,11 @@
pragma solidity ^0.8.0;
import {ECDSA} from "openzeppelin-contracts/contracts/utils/cryptography/ECDSA.sol";
import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol";
/*//////////////////////////////////////////////////////////////////////////
THIS CONTRACT IS STILL IN ACTIVE DEVELOPMENT. NOT FOR PRODUCTION USE
//////////////////////////////////////////////////////////////////////////*/
contract Enum {
enum Operation {
@@ -114,7 +119,9 @@ contract SafeECDSARecoveryPlugin {
}
bytes32 currentOwnerHash = keccak256(abi.encodePacked(currentOwner));
bytes32 ethSignedHash = currentOwnerHash.toEthSignedMessageHash();
bytes32 ethSignedHash = MessageHashUtils.toEthSignedMessageHash(
currentOwnerHash
);
if (newOwner != ethSignedHash.recover(newOwnerSignature))
revert INVALID_NEW_OWNER_SIGNATURE();

View File

@@ -2,11 +2,15 @@
pragma solidity >=0.8.0 <0.9.0;
import {HandlerContext} from "safe-contracts/contracts/handler/HandlerContext.sol";
import {BaseAccount} from "account-abstraction/contracts/core/BaseAccount.sol";
import {IEntryPoint, UserOperation} from "account-abstraction/contracts/interfaces/IEntryPoint.sol";
import {BaseAccount} from "account-abstraction/core/BaseAccount.sol";
import {IEntryPoint, PackedUserOperation} from "account-abstraction/interfaces/IEntryPoint.sol";
import {WebAuthn} from "../primitives/WebAuthn.sol";
import {Safe4337Base} from "./utils/Safe4337Base.sol";
import {Safe4337Base, SIG_VALIDATION_FAILED} from "./utils/Safe4337Base.sol";
/*//////////////////////////////////////////////////////////////////////////
THIS CONTRACT IS STILL IN ACTIVE DEVELOPMENT. NOT FOR PRODUCTION USE
//////////////////////////////////////////////////////////////////////////*/
interface ISafe {
function enableModule(address module) external;
@@ -66,7 +70,7 @@ contract SafeWebAuthnPlugin is Safe4337Base, WebAuthn {
}
function _validateSignature(
UserOperation calldata userOp,
PackedUserOperation calldata userOp,
bytes32 /*userOpHash*/
) internal override returns (uint256 validationData) {
bytes calldata authenticatorData;
@@ -80,7 +84,10 @@ contract SafeWebAuthnPlugin is Safe4337Base, WebAuthn {
// parse length of all fixed-length params (including length)
uint i = 0;
uint dataLen = 32;
uint256 paramLen = abi.decode(userOp.signature[i:i+dataLen], (uint256));
uint256 paramLen = abi.decode(
userOp.signature[i:i + dataLen],
(uint256)
);
// Fixed-length params (bytes1, (uint256?), bytes32, uint256, uint256[2], uint256[2]). Expect 9 slots (288 bytes)
i += dataLen; // advance index
@@ -88,12 +95,12 @@ contract SafeWebAuthnPlugin is Safe4337Base, WebAuthn {
dataLen = paramLen - 32; // length already read
dataLen -= 2 * 2 * 32; // exclude fixed length arrays
(
wrapper.authenticatorDataFlagMask,
, // some number
wrapper.authenticatorDataFlagMask, // some number
,
wrapper.clientChallenge,
wrapper.clientChallengeDataOffset
) = abi.decode(
userOp.signature[i:i+dataLen],
userOp.signature[i:i + dataLen],
(
bytes1,
uint256, //not sure what is encoded here
@@ -103,38 +110,37 @@ contract SafeWebAuthnPlugin is Safe4337Base, WebAuthn {
);
i += dataLen; // advance index
bytes calldata calldataLocation;
// load fixed length array params (pointers to calldata)
dataLen = 2 * 32;
calldataLocation = userOp.signature[i:i+dataLen];
assembly{
calldataLocation = userOp.signature[i:i + dataLen];
assembly {
signature := calldataLocation.offset
}
i += dataLen; // advance index
calldataLocation = userOp.signature[i:i+dataLen];
assembly{
calldataLocation = userOp.signature[i:i + dataLen];
assembly {
pubKey := calldataLocation.offset
}
i += dataLen; // advance index
// parse length of authenticatorData
dataLen = 32;
paramLen = abi.decode(userOp.signature[i:i+dataLen], (uint256));
paramLen = abi.decode(userOp.signature[i:i + dataLen], (uint256));
i += dataLen; // advance index
// assign authenticatorData to sig splice
dataLen = paramLen;
authenticatorData = userOp.signature[i:i+dataLen];
authenticatorData = userOp.signature[i:i + dataLen];
i += ((dataLen >> 5) + 1) << 5; // advance index (round up to next slot)
// parse length of clientData
dataLen = 32;
paramLen = abi.decode(userOp.signature[i:i+dataLen], (uint256));
paramLen = abi.decode(userOp.signature[i:i + dataLen], (uint256));
i += dataLen; // advance index
// assign clientData to sig splice
dataLen = paramLen;
clientData = userOp.signature[i:i+dataLen];
clientData = userOp.signature[i:i + dataLen];
// i += ((dataLen >> 5) + 1) << 5; // advance index (round up to next slot)
} // end scope to free vars from stack

View File

@@ -6,11 +6,15 @@ import {Safe} from "safe-contracts/contracts/Safe.sol";
import {SafeProxyFactory} from "safe-contracts/contracts/proxies/SafeProxyFactory.sol";
import {SafeProxy} from "safe-contracts/contracts/proxies/SafeProxy.sol";
import {EntryPoint} from "account-abstraction/contracts/core/EntryPoint.sol";
import {EntryPoint} from "account-abstraction/core/EntryPoint.sol";
import {SafeZKPPasswordPlugin} from "./SafeZKPPasswordPlugin.sol";
import {IGroth16Verifier} from "./interface/IGroth16Verifier.sol";
/*//////////////////////////////////////////////////////////////////////////
THIS CONTRACT IS STILL IN ACTIVE DEVELOPMENT. NOT FOR PRODUCTION USE
//////////////////////////////////////////////////////////////////////////*/
contract SafeZKPPasswordFactory {
function create(
Safe safeSingleton,

View File

@@ -4,8 +4,12 @@ pragma abicoder v2;
import {IGroth16Verifier} from "./interface/IGroth16Verifier.sol";
import {ISafe} from "./interface/ISafe.sol";
import {Safe4337Base} from "./utils/Safe4337Base.sol";
import {IEntryPoint, UserOperation} from "account-abstraction/contracts/interfaces/IEntryPoint.sol";
import {Safe4337Base, SIG_VALIDATION_FAILED} from "./utils/Safe4337Base.sol";
import {IEntryPoint, PackedUserOperation} from "account-abstraction/interfaces/IEntryPoint.sol";
/*//////////////////////////////////////////////////////////////////////////
THIS CONTRACT IS STILL IN ACTIVE DEVELOPMENT. NOT FOR PRODUCTION USE
//////////////////////////////////////////////////////////////////////////*/
struct ZKPPasswordOwnerStorage {
address owner;
@@ -76,7 +80,7 @@ contract SafeZKPPasswordPlugin is Safe4337Base {
}
function _validateSignature(
UserOperation calldata userOp,
PackedUserOperation calldata userOp,
bytes32 userOpHash
) internal view override returns (uint256 validationData) {
// TODO (merge-ok) There is likely a more efficient way to encode this

View File

@@ -1,30 +1,36 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import {MockGroth16Verifier} from "./utils/MockGroth16Verifier.sol";
import {MockDKIMRegsitry} from "./utils/MockDKIMRegsitry.sol";
import {IDKIMRegsitry} from "./interface/IDKIMRegsitry.sol";
import {ISafe} from "./utils/Safe4337Base.sol";
import {EmailAccountRecoveryRouter} from "./EmailAccountRecoveryRouter.sol";
import {EmailAccountRecovery} from "ether-email-auth/packages/contracts/src/EmailAccountRecovery.sol";
interface ISafeECDSAPlugin {
function getOwner(address safe) external view returns (address);
}
/*//////////////////////////////////////////////////////////////////////////
THIS CONTRACT IS STILL IN ACTIVE DEVELOPMENT. NOT FOR PRODUCTION USE
//////////////////////////////////////////////////////////////////////////*/
struct RecoveryRequest {
bytes32 recoveryHash;
bytes32 dkimPublicKeyHash;
uint256 executeAfter;
address ownerToSwap;
address pendingNewOwner;
uint256 delay;
}
struct GuardianRequest {
address safe;
bool accepted;
}
struct SafeAccountInfo {
address safe;
address previousOwnerInLinkedList;
}
/**
* A safe plugin that recovers a safe ecdsa plugin owner via a zkp of an email.
* A safe plugin that recovers a safe owner via a zkp of an email.
* NOT FOR PRODUCTION USE
*/
contract SafeZkEmailRecoveryPlugin {
/** Default DKIM public key hashes registry */
IDKIMRegsitry public immutable defaultDkimRegistry;
contract SafeZkEmailRecoveryPlugin is EmailAccountRecovery {
/** Default delay has been set to a large timeframe on purpose. Please use a default delay suited to your specific context */
uint256 public constant defaultDelay = 2 weeks;
@@ -33,32 +39,32 @@ contract SafeZkEmailRecoveryPlugin {
/** Mapping of safe address to recovery request */
mapping(address => RecoveryRequest) public recoveryRequests;
/** Mapping of safe address to a custom recovery delay */
mapping(address => uint256) public recoveryDelay;
/** Mapping of guardian address to guardian request */
mapping(address => GuardianRequest) public guardianRequests;
/** Mapping of email account recovery router contracts to safe details needed to complete recovery */
mapping(address => SafeAccountInfo) public recoveryRouterToSafeInfo;
/** Mapping of safe account addresses to email account recovery router contracts**/
/** These are stored for frontends to easily find the router contract address from the given safe account address**/
mapping(address => address) public safeAddrToRecoveryRouter;
/** Mapping of safe address to dkim registry address */
mapping(address => address) public dkimRegistryOfSafe;
// TODO How can we use a custom DKIM reigstry/key with email auth?
// mapping(address => address) public dkimRegistryOfSafe;
/** Errors */
error MODULE_NOT_ENABLED();
error INVALID_OWNER(address expectedOwner, address owner);
error INVALID_OWNER(address owner);
error INVALID_NEW_OWNER();
error RECOVERY_ALREADY_INITIATED();
error RECOVERY_NOT_CONFIGURED();
error INVALID_DKIM_KEY_HASH(
address safe,
string emailDomain,
bytes32 dkimPublicKeyHash
);
error INVALID_PROOF();
error RECOVERY_NOT_INITIATED();
error DELAY_NOT_PASSED();
/** Events */
event RecoveryConfigured(
address indexed safe,
address ecsdaPlugin,
address indexed owner,
bytes32 recoveryHash,
bytes32 dkimPublicKeyHash,
address dkimRegistry,
uint256 customDelay
);
event RecoveryInitiated(
@@ -66,33 +72,157 @@ contract SafeZkEmailRecoveryPlugin {
address newOwner,
uint256 executeAfter
);
event PluginRecovered(
event OwnerRecovered(
address indexed safe,
address ecdsaPlugin,
address oldOwner,
address newOwner
);
event RecoveryCancelled(address indexed safe);
event RecoveryDelaySet(address indexed safe, uint256 delay);
MockGroth16Verifier public immutable verifier;
constructor(
address _verifier,
address _dkimRegistry,
address _emailAuthImpl
) {
verifierAddr = _verifier;
dkimAddr = _dkimRegistry;
emailAuthImplementationAddr = _emailAuthImpl;
}
constructor(address _verifier, address _defaultDkimRegistry) {
verifier = MockGroth16Verifier(_verifier);
defaultDkimRegistry = IDKIMRegsitry(_defaultDkimRegistry);
/**
* EmailAccountRecovery implementations
*/
RECOVERY_HASH_DOMAIN = keccak256(
abi.encode(
keccak256(
"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"
),
keccak256("SafeZKEmailRecoveryPlugin"),
keccak256("1"),
block.chainid,
address(this)
)
/**
* @inheritdoc EmailAccountRecovery
*/
function acceptanceSubjectTemplates()
public
pure
override
returns (string[][] memory)
{
string[][] memory templates = new string[][](1);
templates[0] = new string[](5);
templates[0][0] = "Accept";
templates[0][1] = "guardian";
templates[0][2] = "request";
templates[0][3] = "for";
templates[0][4] = "{ethAddr}";
return templates;
}
/**
* @inheritdoc EmailAccountRecovery
*/
function recoverySubjectTemplates()
public
pure
override
returns (string[][] memory)
{
string[][] memory templates = new string[][](1);
templates[0] = new string[](7);
templates[0][0] = "Update";
templates[0][1] = "owner";
templates[0][2] = "to";
templates[0][3] = "{ethAddr}";
templates[0][4] = "on";
templates[0][5] = "account";
templates[0][6] = "{ethAddr}";
return templates;
}
function acceptGuardian(
address guardian,
uint templateIdx,
bytes[] memory subjectParams,
bytes32
) internal override {
require(guardian != address(0), "invalid guardian");
// TODO extract to function or modifier?
require(
guardianRequests[guardian].safe != address(0),
"guardian not requested"
);
require(
!guardianRequests[guardian].accepted,
"guardian has already accepted"
);
require(templateIdx == 0, "invalid template index");
require(subjectParams.length == 1, "invalid subject params");
address safeInEmail = abi.decode(subjectParams[0], (address));
address safeForRouter = recoveryRouterToSafeInfo[msg.sender].safe;
require(safeForRouter == safeInEmail, "invalid account for router");
require(
guardianRequests[guardian].safe == safeInEmail,
"invalid account in email"
);
guardianRequests[guardian].accepted = true;
}
function processRecovery(
address guardian,
uint templateIdx,
bytes[] memory subjectParams,
bytes32
) internal override {
require(guardian != address(0), "invalid guardian");
require(
guardianRequests[guardian].safe != address(0),
"guardian not requested"
);
require(
guardianRequests[guardian].accepted,
"guardian has not accepted"
);
require(templateIdx == 0, "invalid template index");
require(subjectParams.length == 2, "invalid subject params");
address newOwnerInEmail = abi.decode(subjectParams[0], (address));
require(newOwnerInEmail != address(0), "invalid new owner in email");
address safeInEmail = abi.decode(subjectParams[1], (address));
address safeForRouter = recoveryRouterToSafeInfo[msg.sender].safe;
require(safeForRouter == safeInEmail, "invalid account for router");
require(
guardianRequests[guardian].safe == safeInEmail,
"invalid account in email"
);
bool isExistingOwner = ISafe(safeInEmail).isOwner(newOwnerInEmail);
if (isExistingOwner) revert INVALID_NEW_OWNER();
RecoveryRequest memory recoveryRequest = recoveryRequests[safeInEmail];
if (recoveryRequest.executeAfter > 0) {
revert RECOVERY_ALREADY_INITIATED();
}
uint256 executeAfter = block.timestamp +
recoveryRequests[safeInEmail].delay;
recoveryRequests[safeInEmail].executeAfter = executeAfter;
recoveryRequests[safeInEmail].pendingNewOwner = newOwnerInEmail;
emit RecoveryInitiated(safeInEmail, newOwnerInEmail, executeAfter);
}
function completeRecovery() public override {
SafeAccountInfo memory safeAccountInfo = recoveryRouterToSafeInfo[
msg.sender
];
recoverPlugin(
safeAccountInfo.safe,
safeAccountInfo.previousOwnerInLinkedList
);
}
/**
* Plugin
*/
/**
* @notice Returns recovery request accociated with a safe address
* @param safe address to query storage with
@@ -104,156 +234,129 @@ contract SafeZkEmailRecoveryPlugin {
}
/**
* @notice Stores a recovery hash that can be used to recover a ecdsa plugin
* @notice Returns guardian request accociated with a safe address
* @param safe address to query storage with
*/
function getGuardianRequest(
address safe
) external view returns (GuardianRequest memory) {
return guardianRequests[safe];
}
// TODO test
/**
* @notice Returns the recovery router address that corresponds to the specified Safe account
* @param safe address to query storage with
*/
function getRouterForSafe(address safe) external view returns (address) {
return safeAddrToRecoveryRouter[safe];
}
/**
* @notice Stores a recovery hash that can be used to recover a safe owner
* at a later stage.
* @dev dkimRegistry can be a zero address if the user wants to use the
* defaultDkimRegistry. customDelay can be 0 if the user wants to use defaultDelay
* This function assumes it is being called from a safe - see how msg.sender
* is interpreted. This is the first function that must be called when setting up recovery.
* @param ecsdaPlugin Safe ecsda plugin address that this function will be adding a recovery option for
* @param owner Owner of the ecdsa plugin
* @param recoveryHash Hash of domain, email and salt - keccak256(abi.encodePacked(RECOVERY_HASH_DOMAIN, email, salt))
* @param dkimPublicKeyHash Hash of DKIM public key - keccak256(abi.encodePacked(dkimPublicKey))
* @param dkimRegistry Address of a user-defined DKIM registry
* @param owner Owner on the safe being recovered
* @param guardian The EmailAuth guardian address that has permissions to recover an owner on the account
* @param customDelay A custom delay to set the recoveryDelay value that is associated with a safe.
* @param previousOwnerInLinkedList The previous owner stored in the Safe owners linked list.
* This is needed to rotate the owner at the end of the recovery flow
*/
function configureRecovery(
address ecsdaPlugin,
address owner,
bytes32 recoveryHash,
bytes32 dkimPublicKeyHash,
address dkimRegistry,
uint256 customDelay
) external {
address guardian,
uint256 customDelay,
address previousOwnerInLinkedList // TODO: We should try fetch this automatically when needed. It is possible that owners are changed without going through the recovery plugin and this value could be outdated
) external returns (address emailAccountRecoveryRouterAddress) {
address safe = msg.sender;
bool moduleEnabled = ISafe(safe).isModuleEnabled(address(this));
if (!moduleEnabled) revert MODULE_NOT_ENABLED();
address expectedOwner = ISafeECDSAPlugin(ecsdaPlugin).getOwner(safe);
if (owner != expectedOwner) revert INVALID_OWNER(expectedOwner, owner);
require(
guardianRequests[guardian].safe == address(0),
"guardian already requested"
);
if (recoveryRequests[safe].executeAfter > 0) {
bool isOwner = ISafe(safe).isOwner(owner);
if (!isOwner) revert INVALID_OWNER(owner);
if (recoveryRequests[guardian].executeAfter > 0) {
revert RECOVERY_ALREADY_INITIATED();
}
require(
safeAddrToRecoveryRouter[safe] == address(0),
"router contract for safe already exits"
);
EmailAccountRecoveryRouter emailAccountRecoveryRouter = new EmailAccountRecoveryRouter(
address(this)
);
emailAccountRecoveryRouterAddress = address(emailAccountRecoveryRouter);
require(
recoveryRouterToSafeInfo[emailAccountRecoveryRouterAddress].safe ==
address(0),
"safe for the router contract already exits"
);
recoveryRouterToSafeInfo[
emailAccountRecoveryRouterAddress
] = SafeAccountInfo(safe, previousOwnerInLinkedList);
safeAddrToRecoveryRouter[safe] = emailAccountRecoveryRouterAddress;
uint256 delay = defaultDelay;
if (customDelay > 0) {
recoveryDelay[safe] = customDelay;
} else {
recoveryDelay[safe] = defaultDelay;
delay = customDelay;
}
recoveryRequests[safe] = RecoveryRequest({
recoveryHash: recoveryHash,
dkimPublicKeyHash: dkimPublicKeyHash,
executeAfter: 0,
pendingNewOwner: address(0)
ownerToSwap: owner,
pendingNewOwner: address(0),
delay: delay
});
dkimRegistryOfSafe[safe] = dkimRegistry;
emit RecoveryConfigured(
safe,
ecsdaPlugin,
owner,
recoveryHash,
dkimPublicKeyHash,
dkimRegistry,
customDelay
);
guardianRequests[guardian] = GuardianRequest({
safe: safe,
accepted: false
});
emit RecoveryConfigured(safe, owner, delay);
}
/**
* @notice Initiates a recovery of a safe ecdsa plugin using a zk email proof.
* @dev Rotates the safe ecdsa plugin owner address to a new address. Uses the
* default delay period if no custom delay has been set. This is the second
* function that should be called in the recovery process - after configureRecovery
* @param safe The safe that manages the safe ecdsa plugin being recovered
* @param newOwner The new owner address of the safe ecdsa plugin
* @param emailDomain Domain name of the sender's email
* @param a Part of the proof
* @param b Part of the proof
* @param c Part of the proof
*/
function initiateRecovery(
address safe,
address newOwner,
string memory emailDomain,
uint256[2] memory a,
uint256[2][2] memory b,
uint256[2] memory c
) external {
RecoveryRequest memory recoveryRequest = recoveryRequests[safe];
if (recoveryRequest.recoveryHash == bytes32(0)) {
revert RECOVERY_NOT_CONFIGURED();
}
if (recoveryRequest.executeAfter > 0) {
revert RECOVERY_ALREADY_INITIATED();
}
if (
!this.isDKIMPublicKeyHashValid(
safe,
emailDomain,
recoveryRequest.dkimPublicKeyHash
)
) {
revert INVALID_DKIM_KEY_HASH(
safe,
emailDomain,
recoveryRequest.dkimPublicKeyHash
);
}
uint256[4] memory publicSignals = [
uint256(uint160(safe)),
uint256(recoveryRequest.recoveryHash),
uint256(uint160(newOwner)),
uint256(recoveryRequest.dkimPublicKeyHash)
];
// verify proof
bool verified = verifier.verifyProof(a, b, c, publicSignals);
if (!verified) revert INVALID_PROOF();
uint256 executeAfter = block.timestamp + recoveryDelay[safe];
recoveryRequests[safe].executeAfter = executeAfter;
recoveryRequests[safe].pendingNewOwner = newOwner;
emit RecoveryInitiated(safe, newOwner, executeAfter);
}
/**
* @notice Recovers a safe ecdsa plugin using a zk email proof.
* @dev Rotates the safe ecdsa plugin owner address to a new address.
* @notice Recovers a safe owner using a zk email proof.
* @dev Rotates the safe owner address to a new address.
* This function is designed so it can be called from any account and account type.
* This function is the third and final function that needs to be called in the
* recovery process. After configureRecovery & initiateRecovery
* @param safe The safe that manages the safe ecdsa plugin being recovered
* @param ecdsaPlugin Safe ecsda plugin address that this function will be rotating the owner address for
* @param safe The safe for the owner being rotated
* @param previousOwner The previous owner in the safe owners linked list // TODO: (merge-ok) retrieve this automatically
*/
function recoverPlugin(address safe, address ecdsaPlugin) external {
function recoverPlugin(address safe, address previousOwner) public {
RecoveryRequest memory recoveryRequest = recoveryRequests[safe];
if (recoveryRequest.executeAfter == 0) {
revert RECOVERY_NOT_INITIATED();
}
if (block.timestamp > recoveryRequest.executeAfter) {
if (block.timestamp >= recoveryRequest.executeAfter) {
delete recoveryRequests[safe];
bytes memory data = abi.encodeWithSignature(
"enable(bytes)",
abi.encodePacked(recoveryRequest.pendingNewOwner)
"swapOwner(address,address,address)",
previousOwner,
recoveryRequest.ownerToSwap,
recoveryRequest.pendingNewOwner
);
ISafe(safe).execTransactionFromModule(ecdsaPlugin, 0, data, 0);
ISafe(safe).execTransactionFromModule(safe, 0, data, 0);
emit PluginRecovered(
emit OwnerRecovered(
safe,
ecdsaPlugin,
recoveryRequest.ownerToSwap,
recoveryRequest.pendingNewOwner
);
} else {
@@ -271,38 +374,4 @@ contract SafeZkEmailRecoveryPlugin {
delete recoveryRequests[safe];
emit RecoveryCancelled(safe);
}
/**
* @notice Sets a custom delay for recovering a plugin for a specific safe.
* @dev Custom delay is used instead of the default delay when recovering a
* plugin. Custom delays should be configured with care as they can be
* used to bypass the default delay.
* @param delay The custom delay to be used when recovering a plugin for the safe
*/
function setRecoveryDelay(uint256 delay) external {
address safe = msg.sender;
recoveryDelay[safe] = delay;
emit RecoveryDelaySet(safe, delay);
}
/// @notice Return the DKIM public key hash for a given email domain and safe address
/// @param safe The address of the safe that controls the plugin
/// @param emailDomain Email domain for which the DKIM public key hash is to be returned
function isDKIMPublicKeyHashValid(
address safe,
string memory emailDomain,
bytes32 publicKeyHash
) public view returns (bool) {
address dkimRegistry = dkimRegistryOfSafe[safe];
if (dkimRegistry == address(0)) {
dkimRegistry = address(defaultDkimRegistry);
}
return
IDKIMRegsitry(dkimRegistry).isDKIMPublicKeyHashValid(
emailDomain,
publicKeyHash
);
}
}

View File

@@ -4,6 +4,17 @@ pragma abicoder v2;
import {IGroth16Verifier} from "../interface/IGroth16Verifier.sol";
struct EmailProof {
string domainName; // Domain name of the sender's email
bytes32 publicKeyHash; // Hash of the DKIM public key used in email/proof
uint timestamp; // Timestamp of the email
string maskedSubject; // Masked subject of the email
bytes32 emailNullifier; // Nullifier of the email to prevent its reuse.
bytes32 accountSalt; // Create2 salt of the account
bool isCodeExist; // Check if the account code is exist
bytes proof; // ZK Proof of Email
}
// Mock/stub of snarkjs Groth16 Solidity verifier.
// We can't allow the result to change via a flag in storage as
// that would break ERC-4337 validation storage rules.
@@ -43,4 +54,12 @@ contract MockGroth16Verifier is IGroth16Verifier {
r = true;
}
function verifyEmailProof(
EmailProof memory proof
) public view returns (bool) {
proof;
return true;
}
}

View File

@@ -3,8 +3,8 @@ pragma solidity >=0.7.0 <0.9.0;
pragma abicoder v2;
import {HandlerContext} from "safe-contracts/contracts/handler/HandlerContext.sol";
import {BaseAccount} from "account-abstraction/contracts/core/BaseAccount.sol";
import {SIG_VALIDATION_FAILED} from "account-abstraction/core/Helpers.sol";
import {BaseAccount} from "account-abstraction/core/BaseAccount.sol";
interface ISafe {
/**
@@ -33,6 +33,12 @@ interface ISafe {
* @return True if the module is enabled
*/
function isModuleEnabled(address module) external view returns (bool);
/**
* @notice Returns if `owner` is an owner of the Safe.
* @return Boolean if owner is an owner of the Safe.
*/
function isOwner(address owner) external view returns (bool);
}
/**

View File

@@ -14,6 +14,6 @@ import {MultiSendCallOnly} from "safe-contracts/contracts/libraries/MultiSendCal
import {SignMessageLib} from "safe-contracts/contracts/libraries/SignMessageLib.sol";
import {SafeL2} from "safe-contracts/contracts/SafeL2.sol";
import {Safe} from "safe-contracts/contracts/Safe.sol";
import {EntryPoint} from "account-abstraction/contracts/core/EntryPoint.sol";
import {SimpleAccountFactory} from "account-abstraction/contracts/samples/SimpleAccountFactory.sol";
import {BLSSignatureAggregator} from "account-abstraction/contracts/samples/bls/BLSSignatureAggregator.sol";
import {EntryPoint} from "account-abstraction/core/EntryPoint.sol";
import {SimpleAccountFactory} from "account-abstraction/samples/SimpleAccountFactory.sol";
import {BLSSignatureAggregator} from "account-abstraction/samples/bls/BLSSignatureAggregator.sol";

View File

@@ -7,13 +7,14 @@ import {
import { setupTests } from "./utils/setupTests";
import receiptOf from "./utils/receiptOf";
import {
generateInitCodeAndAddress,
generateFactoryParamsAndAddress,
createUserOperation,
} from "./utils/createUserOp";
import { getSigners } from "./utils/getSigners";
import getBlsUserOpHash from "./utils/getBlsUserOpHash";
import appendKeyToInitCode from "./utils/appendKeyToInitCode";
import setupBls from "./utils/setupBls";
import { packUserOp } from "./utils/userOpUtils";
const BLS_PRIVATE_KEY =
"0xdbe3d601b1b25c42c50015a87855fdce00ea9b3a7e33c92d31c69aeb70708e08";
@@ -58,41 +59,46 @@ describe("SafeBlsPlugin", () => {
[recipientAddress, transferAmount, "0x"],
);
let { initCode, deployedAddress } = await generateInitCodeAndAddress(
admin,
owner,
safeBlsPlugin,
safeSingleton,
safeProxyFactory,
);
let { factoryParams, deployedAddress } =
await generateFactoryParamsAndAddress(
admin,
owner,
safeBlsPlugin,
safeSingleton,
safeProxyFactory,
);
initCode = appendKeyToInitCode(initCode, blsSigner.pubkey);
factoryParams.factoryData = appendKeyToInitCode(
factoryParams.factoryData,
blsSigner.pubkey,
);
const unsignedUserOperation = await createUserOperation(
provider,
bundlerProvider,
deployedAddress,
initCode,
factoryParams,
userOpCallData,
entryPointAddress,
"0x",
);
const packedUserOperation = packUserOp(unsignedUserOperation);
const blsUserOpHash = getBlsUserOpHash(
(await provider.getNetwork()).chainId,
await blsSignatureAggregator.getAddress(),
blsSigner.pubkey,
unsignedUserOperation,
packedUserOperation,
entryPointAddress,
);
const aggReportedUserOpHash = await blsSignatureAggregator.getUserOpHash(
unsignedUserOperation,
);
const aggReportedUserOpHash =
await blsSignatureAggregator.getUserOpHash(packedUserOperation);
expect(blsUserOpHash).to.equal(aggReportedUserOpHash);
const userOperation = {
...unsignedUserOperation,
...packedUserOperation,
signature: solidityPacked(
["uint256[2]"],
[blsSigner.sign(blsUserOpHash)],

View File

@@ -1,5 +1,5 @@
import { expect } from "chai";
import { AbiCoder, ethers, solidityPacked } from "ethers";
import { ethers, solidityPacked } from "ethers";
import {
AddressRegistry__factory,
EntryPoint__factory,
@@ -12,6 +12,7 @@ import { setupTests } from "./utils/setupTests";
import { createUserOperation } from "./utils/createUserOp";
import setupBls from "./utils/setupBls";
import getBlsUserOpHash from "./utils/getBlsUserOpHash";
import { packUserOp } from "./utils/userOpUtils";
const BLS_PRIVATE_KEY =
"0xdbe3d601b1b25c42c50015a87855fdce00ea9b3a7e33c92d31c69aeb70708e08";
@@ -41,7 +42,6 @@ describe("SafeCompressionPlugin", () => {
SafeCompressionFactory__factory,
[],
);
await safeCompressionFactory.waitForDeployment();
const addressRegistry = await deployer.connectOrDeploy(
AddressRegistry__factory,
@@ -92,11 +92,14 @@ describe("SafeCompressionPlugin", () => {
[compressedActions],
);
// Note: initCode is not used because we need to create both the safe
// Note: factoryParams is not used because we need to create both the safe
// proxy and the plugin, and 4337 currently only allows one contract
// creation in this step. Since we need an extra step anyway, it's simpler
// to do the whole create outside of 4337.
const initCode = "0x";
const factoryParams = {
factory: "0x",
factoryData: "0x",
};
// Native tokens for the pre-fund
await receiptOf(
@@ -112,27 +115,28 @@ describe("SafeCompressionPlugin", () => {
provider,
bundlerProvider,
accountAddress,
initCode,
factoryParams,
userOpCallData,
entryPointAddress,
"0x",
);
const packedUserOperation = packUserOp(unsignedUserOperation);
const blsUserOpHash = getBlsUserOpHash(
(await provider.getNetwork()).chainId,
await blsSignatureAggregator.getAddress(),
blsSigner.pubkey,
unsignedUserOperation,
packedUserOperation,
entryPointAddress,
);
const aggReportedUserOpHash = await blsSignatureAggregator.getUserOpHash(
unsignedUserOperation,
);
const aggReportedUserOpHash =
await blsSignatureAggregator.getUserOpHash(packedUserOperation);
expect(blsUserOpHash).to.equal(aggReportedUserOpHash);
const userOperation = {
...unsignedUserOperation,
...packedUserOperation,
signature: solidityPacked(
["uint256[2]"],
[blsSigner.sign(blsUserOpHash)],

View File

@@ -64,11 +64,14 @@ describe("SafeECDSAPlugin", () => {
[recipient.address, transferAmount, "0x00"],
);
// Note: initCode is not used because we need to create both the safe
// Note: factoryParams is not used because we need to create both the safe
// proxy and the plugin, and 4337 currently only allows one contract
// creation in this step. Since we need an extra step anyway, it's simpler
// to do the whole create outside of 4337.
const initCode = "0x";
const factoryParams = {
factory: "0x",
factoryData: "0x",
};
// Send userOp
await createAndSendUserOpWithEcdsaSig(
@@ -76,7 +79,7 @@ describe("SafeECDSAPlugin", () => {
bundlerProvider,
owner,
accountAddress,
initCode,
factoryParams,
userOpCallData,
entryPointAddress,
dummySignature,

View File

@@ -170,7 +170,10 @@ describe("SafeECDSARecoveryPlugin", () => {
[await recoveryPlugin.getAddress(), "0x00", addRecoveryAccountCalldata],
);
const initCode = "0x";
const factoryParams = {
factory: "0x",
factoryData: "0x",
};
const dummySignature = await owner.signMessage("dummy sig");
// Send userOp to add recovery account
@@ -179,7 +182,7 @@ describe("SafeECDSARecoveryPlugin", () => {
bundlerProvider,
owner,
safeProxyAddress,
initCode,
factoryParams,
userOpCallData,
entryPointAddress,
dummySignature,
@@ -236,7 +239,7 @@ describe("SafeECDSARecoveryPlugin", () => {
bundlerProvider,
newEcdsaPluginSigner,
safeProxyAddress,
initCode,
factoryParams,
userOpCallData,
entryPointAddress,
dummySignature,
@@ -317,7 +320,10 @@ describe("SafeECDSARecoveryPlugin", () => {
[await recoveryPlugin.getAddress(), "0x00", addRecoveryAccountCalldata],
);
const initCode = "0x";
const factoryParams = {
factory: "0x",
factoryData: "0x",
};
const dummySignature = await owner.signMessage("dummy sig");
// Send userOp to add recovery account
@@ -326,7 +332,7 @@ describe("SafeECDSARecoveryPlugin", () => {
bundlerProvider,
owner,
safeProxyAddress,
initCode,
factoryParams,
userOpCallData,
entryPointAddress,
dummySignature,
@@ -380,7 +386,7 @@ describe("SafeECDSARecoveryPlugin", () => {
bundlerProvider,
guardianSigner,
guardianSimpleAccountAddress,
initCode,
factoryParams,
userOpCallData,
entryPointAddress,
dummySignature,
@@ -403,7 +409,7 @@ describe("SafeECDSARecoveryPlugin", () => {
bundlerProvider,
newEcdsaPluginSigner,
safeProxyAddress,
initCode,
factoryParams,
userOpCallData,
entryPointAddress,
dummySignature,

View File

@@ -4,7 +4,7 @@ import sendUserOpAndWait from "./utils/sendUserOpAndWait";
import { setupTests } from "./utils/setupTests";
import { SafeWebAuthnPlugin__factory } from "../../typechain-types";
import {
generateInitCodeAndAddress,
generateFactoryParamsAndAddress,
createUserOperation,
} from "./utils/createUserOp";
import { getSigners } from "./utils/getSigners";
@@ -91,14 +91,14 @@ describe.skip("SafeWebAuthnPlugin", () => {
"execTransaction",
[recipientAddress, transferAmount, "0x00"],
);
const { initCode, deployedAddress } = await generateInitCodeAndAddress(
admin,
owner,
safeWebAuthnPlugin,
safeSingleton,
safeProxyFactory,
);
const { factoryParams, deployedAddress } =
await generateFactoryParamsAndAddress(
admin,
owner,
safeWebAuthnPlugin,
safeSingleton,
safeProxyFactory,
);
const recipientBalanceBefore = await provider.getBalance(recipientAddress);
@@ -106,7 +106,7 @@ describe.skip("SafeWebAuthnPlugin", () => {
provider,
bundlerProvider,
deployedAddress,
initCode,
factoryParams,
userOpCallData,
entryPointAddress,
userOpSignature,

View File

@@ -1,4 +1,3 @@
import { getUserOpHash } from "@account-abstraction/utils";
import { ERC4337ZKPPasswordClient } from "@getwax/circuits";
import { expect } from "chai";
import { resolveProperties, ethers } from "ethers";
@@ -11,6 +10,7 @@ import {
} from "../../typechain-types";
import { setupTests } from "./utils/setupTests";
import { createUserOperation } from "./utils/createUserOp";
import { getUserOpHash } from "./utils/userOpUtils";
describe("SafeZKPPasswordPlugin", () => {
it("should pass the ERC4337 validation", async () => {
@@ -85,17 +85,20 @@ describe("SafeZKPPasswordPlugin", () => {
],
);
// Note: initCode is not used because we need to create both the safe
// Note: factoryParams is not used because we need to create both the safe
// proxy and the plugin, and 4337 currently only allows one contract
// creation in this step. Since we need an extra step anyway, it's simpler
// to do the whole create outside of 4337.
const initCode = "0x";
const factoryParams = {
factory: "0x",
factoryData: "0x",
};
const unsignedUserOperation = await createUserOperation(
provider,
bundlerProvider,
accountAddress,
initCode,
factoryParams,
userOpCallData,
entryPointAddress,
dummySignature,

View File

@@ -51,7 +51,6 @@ describe("SafeZkEmailRecoveryPlugin", () => {
SafeECDSAFactory__factory,
[],
);
await safeECDSAFactory.waitForDeployment();
const createArgs = [
safeSingleton,
@@ -94,7 +93,6 @@ describe("SafeZkEmailRecoveryPlugin", () => {
await defaultDkimRegistry.getAddress(),
],
);
await recoveryPlugin.waitForDeployment();
});
it("Should use recovery plugin via EOA and then send tx with new key.", async () => {
@@ -173,7 +171,10 @@ describe("SafeZkEmailRecoveryPlugin", () => {
[await recoveryPlugin.getAddress(), "0x00", configureRecoveryCalldata],
);
const initCode = "0x";
const factoryParams = {
factory: "0x",
factoryData: "0x",
};
const dummySignature = await owner.signMessage("dummy sig");
// Send userOp to add recovery account
@@ -182,7 +183,7 @@ describe("SafeZkEmailRecoveryPlugin", () => {
bundlerProvider,
owner,
safeProxyAddress,
initCode,
factoryParams,
userOpCallData,
entryPointAddress,
dummySignature,
@@ -291,7 +292,7 @@ describe("SafeZkEmailRecoveryPlugin", () => {
bundlerProvider,
newEcdsaPluginSigner,
safeProxyAddress,
initCode,
factoryParams,
userOpCallData,
entryPointAddress,
dummySignature,
@@ -393,7 +394,10 @@ describe("SafeZkEmailRecoveryPlugin", () => {
[await recoveryPlugin.getAddress(), "0x00", addRecoveryHashCalldata],
);
const initCode = "0x";
const factoryParams = {
factory: "0x",
factoryData: "0x",
};
const dummySignature = await owner.signMessage("dummy sig");
// Send userOp to add recovery account
@@ -402,7 +406,7 @@ describe("SafeZkEmailRecoveryPlugin", () => {
bundlerProvider,
owner,
safeProxyAddress,
initCode,
factoryParams,
userOpCallData,
entryPointAddress,
dummySignature,
@@ -471,7 +475,7 @@ describe("SafeZkEmailRecoveryPlugin", () => {
bundlerProvider,
otherAccount,
otherSimpleAccountAddress,
initCode,
factoryParams,
userOpCallData,
entryPointAddress,
dummySignature,
@@ -508,7 +512,7 @@ describe("SafeZkEmailRecoveryPlugin", () => {
bundlerProvider,
otherAccount,
otherSimpleAccountAddress,
initCode,
factoryParams,
userOpCallData,
entryPointAddress,
dummySignature,
@@ -531,7 +535,7 @@ describe("SafeZkEmailRecoveryPlugin", () => {
bundlerProvider,
newEcdsaPluginSigner,
safeProxyAddress,
initCode,
factoryParams,
userOpCallData,
entryPointAddress,
dummySignature,

View File

@@ -1,7 +1,5 @@
import { ethers, getBytes, NonceManager, Signer } from "ethers";
import { AddressZero } from "@ethersproject/constants";
import { UserOperationStruct } from "@account-abstraction/contracts";
import { getUserOpHash } from "@account-abstraction/utils";
import { SafeProxyFactory } from "../../../typechain-types/lib/safe-contracts/contracts/proxies/SafeProxyFactory";
import { Safe } from "../../../typechain-types/lib/safe-contracts/contracts/Safe";
@@ -14,10 +12,17 @@ import receiptOf from "./receiptOf";
import { calculateProxyAddress } from "./calculateProxyAddress";
import { getGasEstimates } from "./getGasEstimates";
import sendUserOpAndWait from "./sendUserOpAndWait";
import {
FactoryParams,
getUserOpHash,
PackedUserOperation,
packUserOp,
UserOperation,
} from "./userOpUtils";
type Plugin = SafeBlsPlugin | SafeWebAuthnPlugin;
export const generateInitCodeAndAddress = async (
export const generateFactoryParamsAndAddress = async (
admin: NonceManager,
owner: NonceManager,
plugin: Plugin,
@@ -58,25 +63,24 @@ export const generateInitCodeAndAddress = async (
}),
);
// The initCode contains 20 bytes of the factory address and the rest is the
// calldata to be forwarded
const initCode = ethers.concat([
factoryAddress,
safeProxyFactory.interface.encodeFunctionData("createProxyWithNonce", [
singletonAddress,
encodedInitializer,
73,
]),
]);
const factoryData = safeProxyFactory.interface.encodeFunctionData(
"createProxyWithNonce",
[singletonAddress, encodedInitializer, 73],
);
return { initCode, deployedAddress };
const factoryParams = {
factory: factoryAddress,
factoryData,
};
return { factoryParams, deployedAddress };
};
export const createUserOperation = async (
provider: ethers.JsonRpcProvider,
bundlerProvider: ethers.JsonRpcProvider,
accountAddress: string,
initCode: string,
factoryParams: FactoryParams,
userOpCallData: string,
entryPointAddress: string,
dummySignature: string,
@@ -88,16 +92,19 @@ export const createUserOperation = async (
const nonce = await entryPoint.getNonce(accountAddress, "0x00");
const nonceHex = "0x0" + nonce.toString();
const userOperationWithoutGasFields = {
let userOp: Partial<UserOperation> = {
sender: accountAddress,
nonce: nonceHex,
initCode,
callData: userOpCallData,
callGasLimit: "0x00",
paymasterAndData: "0x",
signature: dummySignature,
};
if (factoryParams.factory !== "0x") {
userOp.factory = factoryParams.factory;
userOp.factoryData = factoryParams.factoryData;
}
const {
callGasLimit,
verificationGasLimit,
@@ -107,23 +114,23 @@ export const createUserOperation = async (
} = await getGasEstimates(
provider,
bundlerProvider,
userOperationWithoutGasFields,
userOp,
entryPointAddress,
);
const unsignedUserOperation = {
sender: accountAddress,
nonce: nonceHex,
initCode,
factory: userOp.factory,
factoryData: userOp.factoryData,
callData: userOpCallData,
callGasLimit,
verificationGasLimit,
preVerificationGas,
maxFeePerGas,
maxPriorityFeePerGas,
paymasterAndData: "0x",
signature: dummySignature,
} satisfies UserOperationStruct;
} satisfies UserOperation;
return await ethers.resolveProperties(unsignedUserOperation);
};
@@ -133,7 +140,7 @@ export const createAndSendUserOpWithEcdsaSig = async (
bundlerProvider: ethers.JsonRpcProvider,
owner: Signer,
accountAddress: string,
initCode: string,
factoryParams: FactoryParams,
userOpCallData: string,
entryPointAddress: string,
dummySignature: string,
@@ -142,7 +149,7 @@ export const createAndSendUserOpWithEcdsaSig = async (
provider,
bundlerProvider,
accountAddress,
initCode,
factoryParams,
userOpCallData,
entryPointAddress,
dummySignature,

View File

@@ -1,12 +1,13 @@
import { ethers, keccak256 } from "ethers";
import { ResolvedUserOp } from "./resolveUserOp";
import { solG2 } from "@thehubbleproject/bls/dist/mcl";
import { PackedUserOperation } from "./userOpUtils";
export default function getBlsUserOpHash(
chainId: bigint,
aggregatorAddress: string,
publicKey: solG2,
userOp: ResolvedUserOp,
userOp: PackedUserOperation,
entryPointAddress: string,
): string {
const abi = ethers.AbiCoder.defaultAbiCoder();
@@ -14,8 +15,14 @@ export default function getBlsUserOpHash(
return keccak256(
abi.encode(
["bytes32", "bytes32", "address", "uint256"],
[internalUserOpHash(userOp), publicKeyHash, aggregatorAddress, chainId],
["bytes32", "bytes32", "address", "uint256", "address"],
[
internalUserOpHash(userOp),
publicKeyHash,
aggregatorAddress,
chainId,
entryPointAddress,
],
),
);
}
@@ -26,7 +33,7 @@ export default function getBlsUserOpHash(
* (Also the same as UserOperationLib.hash, but *different* from EntryPoint's
* getUserOpHash and BLSSignatureAggregator's getUserOpHash.)
*/
function internalUserOpHash(userOp: ResolvedUserOp): string {
function internalUserOpHash(userOp: PackedUserOperation): string {
const abi = ethers.AbiCoder.defaultAbiCoder();
return keccak256(
@@ -36,11 +43,9 @@ function internalUserOpHash(userOp: ResolvedUserOp): string {
"uint256", // userOp.nonce,
"bytes32", // keccak256(userOp.initCode),
"bytes32", // keccak256(userOp.callData),
"uint256", // userOp.callGasLimit,
"uint256", // userOp.verificationGasLimit,
"bytes32", // userOp.accountGasLimits,
"uint256", // userOp.preVerificationGas,
"uint256", // userOp.maxFeePerGas,
"uint256", // userOp.maxPriorityFeePerGas,
"bytes32", // userOp.gasFees,
"bytes32", // keccak256(userOp.paymasterAndData),
],
[
@@ -48,11 +53,9 @@ function internalUserOpHash(userOp: ResolvedUserOp): string {
userOp.nonce,
keccak256(userOp.initCode),
keccak256(userOp.callData),
userOp.callGasLimit,
userOp.verificationGasLimit,
userOp.accountGasLimits,
userOp.preVerificationGas,
userOp.maxFeePerGas,
userOp.maxPriorityFeePerGas,
userOp.gasFees,
keccak256(userOp.paymasterAndData),
],
),

Some files were not shown because too many files have changed in this diff Show More