├── src
├── react-app-env.d.ts
├── qr-seal-logo.png
├── qr-seal-logo-transparent.png
├── singletons
│ └── Schnorr.ts
├── setupTests.ts
├── App.test.tsx
├── index.css
├── config
│ ├── constants.ts
│ ├── service-worker.js
│ └── service-worker-registration.js
├── reportWebVitals.ts
├── auth
│ └── context
│ │ ├── step.tsx
│ │ ├── multisig.tsx
│ │ └── eoa.tsx
├── common
│ ├── accounts
│ │ ├── index.tsx
│ │ └── AccountAddress.tsx
│ ├── QRCodeScanner.js
│ └── styles.css
├── multisig
│ └── components
│ │ ├── JoinMultisig.tsx
│ │ ├── CreateMultisigByScanning.tsx
│ │ ├── CreateTransaction.tsx
│ │ └── CoSign.tsx
├── index.tsx
├── install
│ └── InstallPWA.tsx
├── utils
│ └── helpers.ts
├── logo.svg
├── deploy
│ ├── getBytecodeJs.js
│ └── getBytecode.ts
├── App.tsx
└── builds
│ ├── AmbireAccountFactory.json
│ └── ERC4337Account.json
├── public
├── favicon.ico
├── robots.txt
├── favicon-16x16.png
├── favicon-32x32.png
├── mstile-150x150.png
├── apple-touch-icon.png
├── android-chrome-192x192.png
├── android-chrome-512x512.png
├── browserconfig.xml
├── manifest.json
└── index.html
├── .env.example
├── hardhat.config.js
├── test
├── config.js
├── entryPointSendTxn.test.js
├── DeployERC4337.test.js
├── createSender.test.js
└── DeployFactory.test.js
├── scripts
├── checkBalance.js
├── erc4337
│ ├── config.js
│ ├── queryPimlico.js
│ ├── getNonce.js
│ ├── examplePimlico.js
│ └── ambireAccountViaPimlico.js
├── fund.js
├── deploy.js
└── deployAndExecute.js
├── tsconfig.json
├── craco.config.js
├── contracts
├── libs
│ ├── Bytes.sol
│ ├── Exec.sol
│ └── SignatureValidator.sol
├── SenderCreator.sol
├── EntryPoint.sol
├── ERC4337Account.sol
├── AmbireAccountFactory.sol
└── AmbireAccount.sol
├── README.md
├── package.json
└── .gitignore
/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Oxbobby/qr-seal/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/src/qr-seal-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Oxbobby/qr-seal/HEAD/src/qr-seal-logo.png
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/public/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Oxbobby/qr-seal/HEAD/public/favicon-16x16.png
--------------------------------------------------------------------------------
/public/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Oxbobby/qr-seal/HEAD/public/favicon-32x32.png
--------------------------------------------------------------------------------
/public/mstile-150x150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Oxbobby/qr-seal/HEAD/public/mstile-150x150.png
--------------------------------------------------------------------------------
/public/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Oxbobby/qr-seal/HEAD/public/apple-touch-icon.png
--------------------------------------------------------------------------------
/public/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Oxbobby/qr-seal/HEAD/public/android-chrome-192x192.png
--------------------------------------------------------------------------------
/public/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Oxbobby/qr-seal/HEAD/public/android-chrome-512x512.png
--------------------------------------------------------------------------------
/src/qr-seal-logo-transparent.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Oxbobby/qr-seal/HEAD/src/qr-seal-logo-transparent.png
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | PIMLICO_API_KEY=
2 | DEPLOY_PRIVATE_KEY=
3 | # WARNING: DO NOT USE THE BELOW CONST IN A BUILD. IT WILL EXPOSE THE KEY
4 | REACT_APP_PIMLICO_API_KEY=
--------------------------------------------------------------------------------
/src/singletons/Schnorr.ts:
--------------------------------------------------------------------------------
1 | import Schnorrkel from "@borislav.itskov/schnorrkel.js";
2 |
3 | let schnorrkel: Schnorrkel|null;
4 | export function getSchnorrkelInstance() {
5 | if (!schnorrkel) {
6 | schnorrkel = new Schnorrkel();
7 | }
8 | return schnorrkel;
9 | }
--------------------------------------------------------------------------------
/src/setupTests.ts:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom';
6 |
--------------------------------------------------------------------------------
/hardhat.config.js:
--------------------------------------------------------------------------------
1 | require("@nomicfoundation/hardhat-toolbox")
2 |
3 | module.exports = {
4 | solidity: {
5 | version: "0.8.19",
6 | settings: {
7 | viaIR: true,
8 | optimizer: {
9 | enabled: true,
10 | runs: 1000,
11 | },
12 | },
13 | },
14 | }
--------------------------------------------------------------------------------
/public/browserconfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | #da532c
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/src/App.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render, screen } from '@testing-library/react';
3 | import App from './App';
4 |
5 | test('renders learn react link', () => {
6 | render();
7 | const linkElement = screen.getByText(/learn react/i);
8 | expect(linkElement).toBeInTheDocument();
9 | });
10 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
5 | sans-serif;
6 | -webkit-font-smoothing: antialiased;
7 | -moz-osx-font-smoothing: grayscale;
8 | }
9 |
10 | code {
11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
12 | monospace;
13 | }
14 |
--------------------------------------------------------------------------------
/src/config/constants.ts:
--------------------------------------------------------------------------------
1 | import { ethers } from "ethers";
2 |
3 | // TODO: Change those when we deploy on a specific network
4 | export const FACTORY_ADDRESS = "0xAc1157C17F3CbC5ad6a677B890117C29183FcE79";
5 | export const polygon = 'https://polygon-rpc.com'
6 | export const localhost = 'http://127.0.0.1:8545'
7 | export const mainProvider = new ethers.providers.JsonRpcProvider(polygon)
8 | export const ENTRY_POINT_ADDRESS = "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789"
--------------------------------------------------------------------------------
/test/config.js:
--------------------------------------------------------------------------------
1 | const AmbireAccount = require('../artifacts/contracts/AmbireAccount.sol/AmbireAccount.json')
2 | const AmbireAccountFactory = require('../artifacts/contracts/AmbireAccountFactory.sol/AmbireAccountFactory.json')
3 | const buildInfo = require('../src/builds/FactoryAndAccountBuild.json')
4 | const deployGasLimit = 1000000
5 |
6 | module.exports = {
7 | AmbireAccount,
8 | AmbireAccountFactory,
9 | buildInfo,
10 | deployGasLimit
11 | }
12 |
--------------------------------------------------------------------------------
/src/reportWebVitals.ts:
--------------------------------------------------------------------------------
1 | import { ReportHandler } from 'web-vitals';
2 |
3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => {
4 | if (onPerfEntry && onPerfEntry instanceof Function) {
5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
6 | getCLS(onPerfEntry);
7 | getFID(onPerfEntry);
8 | getFCP(onPerfEntry);
9 | getLCP(onPerfEntry);
10 | getTTFB(onPerfEntry);
11 | });
12 | }
13 | };
14 |
15 | export default reportWebVitals;
16 |
--------------------------------------------------------------------------------
/scripts/checkBalance.js:
--------------------------------------------------------------------------------
1 | const { ethers } = require("ethers")
2 |
3 | const localhost = 'http://127.0.0.1:8545'
4 | const provider = new ethers.providers.JsonRpcProvider(localhost)
5 |
6 | async function checkBalance() {
7 | const balance = await provider.getBalance('0xdd2a7Dc3d038b5EA4164D41B3617aDa5eb4179bf')
8 |
9 | console.log('balance', balance)
10 | }
11 |
12 | // We recommend this pattern to be able to use async/await everywhere
13 | // and properly handle errors.
14 | checkBalance().catch((error) => {
15 | console.error(error)
16 | process.exitCode = 1
17 | });
18 |
--------------------------------------------------------------------------------
/scripts/erc4337/config.js:
--------------------------------------------------------------------------------
1 | const rpcs = {
2 | mumbai: 'https://rpc-mumbai.maticvigil.com',
3 | linea: 'https://rpc.goerli.linea.build/',
4 | polygon: 'https://polygon-rpc.com'
5 | }
6 | const chains = {
7 | linea: 'linea-testnet',
8 | mumbai: 'mumbai',
9 | polygon: 'polygon',
10 | }
11 | const chainIds = {
12 | linea: 59140,
13 | mumbai: 80001,
14 | polygon: 137,
15 | }
16 | const factoryAddr = {
17 | mumbai: '0x153E957A9ff1688BbA982856Acf178524aF96D78',
18 | polygon: '0xAc1157C17F3CbC5ad6a677B890117C29183FcE79',
19 | }
20 |
21 | module.exports = {
22 | rpcs,
23 | chains,
24 | chainIds,
25 | factoryAddr
26 | }
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "esModuleInterop": true,
12 | "allowSyntheticDefaultImports": true,
13 | "strict": true,
14 | "forceConsistentCasingInFileNames": true,
15 | "noFallthroughCasesInSwitch": true,
16 | "module": "esnext",
17 | "moduleResolution": "node",
18 | "resolveJsonModule": true,
19 | "isolatedModules": true,
20 | "noEmit": true,
21 | "jsx": "react-jsx"
22 | },
23 | "include": [
24 | "src"
25 | ]
26 | }
27 |
--------------------------------------------------------------------------------
/craco.config.js:
--------------------------------------------------------------------------------
1 | const webpack = require("webpack");
2 |
3 | module.exports = {
4 | webpack: {
5 | configure: (webpackConfig) => {
6 | // ProvidePlugin for Buffer
7 | webpackConfig.plugins.push(
8 | new webpack.ProvidePlugin({
9 | Buffer: ["buffer", "Buffer"],
10 | })
11 | );
12 |
13 | // Aliases for other modules
14 | webpackConfig.resolve.alias = {
15 | ...webpackConfig.resolve.alias,
16 | crypto: require.resolve("crypto-browserify"),
17 | stream: require.resolve("stream-browserify"),
18 | assert: require.resolve("assert/"),
19 | };
20 |
21 | return webpackConfig;
22 | },
23 | },
24 | };
25 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "QR Seal",
3 | "name": "QR Seal",
4 | "description": "QR-Seal is a secure, user-friendly multisig crypto wallet that leverages Schnorr signatures and QR technology for fast and efficient blockchain transactions.",
5 | "icons": [
6 | {
7 | "src": "favicon.ico",
8 | "sizes": "48x48",
9 | "type": "image/x-icon"
10 | },
11 | {
12 | "src": "android-chrome-192x192.png",
13 | "type": "image/png",
14 | "sizes": "192x192"
15 | },
16 | {
17 | "src": "android-chrome-512x512.png",
18 | "type": "image/png",
19 | "sizes": "512x512"
20 | }
21 | ],
22 | "start_url": ".",
23 | "display": "standalone",
24 | "theme_color": "#000000",
25 | "background_color": "#ffffff"
26 | }
27 |
--------------------------------------------------------------------------------
/src/auth/context/step.tsx:
--------------------------------------------------------------------------------
1 | import React, { createContext, useState, useContext, useCallback } from 'react';
2 |
3 | const StepContext = createContext({});
4 |
5 | const StepProvider = ({ children }: any) => {
6 | const [activeStep, setActiveStep] = useState(() => {
7 | const storedStep = localStorage.getItem('activeStep');
8 | return storedStep ? JSON.parse(storedStep) : 0;
9 | });
10 |
11 | const updateStep = useCallback((newStep: any) => {
12 | setActiveStep(newStep);
13 | localStorage.setItem('activeStep', JSON.stringify(newStep));
14 | }, []);
15 |
16 | return (
17 |
18 | {children}
19 |
20 | );
21 | };
22 |
23 | const useSteps = () => {
24 | const context = useContext(StepContext);
25 | if (!context) {
26 | throw new Error('useSteps must be used within a StepProvider');
27 | }
28 | return context;
29 | };
30 |
31 | export { StepProvider, useSteps };
32 |
--------------------------------------------------------------------------------
/test/entryPointSendTxn.test.js:
--------------------------------------------------------------------------------
1 | const { ethers } = require('hardhat')
2 |
3 | describe('Entry point', function(){
4 | it('should be able to send a txn via the entry point', async function(){
5 | const [signer] = await ethers.getSigners()
6 | const entryPoint = await ethers.deployContract('EntryPoint')
7 | const erc4337 = await ethers.deployContract('ERC4337Account', [entryPoint.address, [signer.address]])
8 | const abi = ['function executeBySender(tuple(address, uint, bytes)[] calldata txns) external payable']
9 | const iface = new ethers.utils.Interface(abi)
10 | const to = entryPoint.address
11 | const value = 0
12 | const data = "0x68656c6c6f" // "hello" encoded to to utf-8 bytes
13 | const singleTxn = [to, value, data]
14 | const txns = [singleTxn]
15 | const calldata = iface.encodeFunctionData('executeBySender', [txns])
16 | const result = await entryPoint.sendTxnOutside(erc4337.address, calldata, ethers.utils.hexlify(100_000))
17 | console.log(result)
18 | })
19 | })
--------------------------------------------------------------------------------
/src/common/accounts/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useContext } from 'react'
2 | import AccountAddress from "./AccountAddress";
3 | import MultisigContext from "../../auth/context/multisig";
4 | import { Flex, Heading } from "@chakra-ui/react"
5 | import { useEOA } from '../../auth/context/eoa';
6 |
7 | const Accounts = () => {
8 | const { eoaAddress, createAndStoreEOA } = useEOA();
9 | const { multisigData } = useContext(MultisigContext)
10 |
11 | return (
12 |
13 | Your Accounts
14 |
15 |
16 |
17 |
18 |
19 |
20 | );
21 | };
22 |
23 | export default Accounts;
24 |
--------------------------------------------------------------------------------
/contracts/libs/Bytes.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: agpl-3.0
2 | pragma solidity 0.8.19;
3 |
4 | library Bytes {
5 | function trimToSize(bytes memory b, uint256 newLen) internal pure {
6 | require(b.length > newLen, 'BytesLib: only shrinking');
7 | assembly {
8 | mstore(b, newLen)
9 | }
10 | }
11 |
12 | /***********************************|
13 | | Read Bytes Functions |
14 | |__________________________________*/
15 |
16 | /**
17 | * @dev Reads a bytes32 value from a position in a byte array.
18 | * @param b Byte array containing a bytes32 value.
19 | * @param index Index in byte array of bytes32 value.
20 | * @return result bytes32 value from byte array.
21 | */
22 | function readBytes32(bytes memory b, uint256 index) internal pure returns (bytes32 result) {
23 | // Arrays are prefixed by a 256 bit length parameter
24 | index += 32;
25 |
26 | require(b.length >= index, 'BytesLib: length');
27 |
28 | // Read the bytes32 from array memory
29 | assembly {
30 | result := mload(add(b, index))
31 | }
32 | return result;
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/scripts/erc4337/queryPimlico.js:
--------------------------------------------------------------------------------
1 | const { StaticJsonRpcProvider } = require("@ethersproject/providers")
2 | const { chains } = require("./config")
3 | require('dotenv').config();
4 |
5 | const run = async () => {
6 | const apiKey = process.env.PIMLICO_API_KEY
7 | const pimlicoEndpoint = `https://api.pimlico.io/v1/${chains.mumbai}/rpc?apikey=${apiKey}`
8 | const pimlicoProvider = new StaticJsonRpcProvider(pimlicoEndpoint)
9 |
10 | const userOperationHash = '0x34f0a32e800dd253243003a0076b888f06e5f9efeabd328af9e9d02e8b15a7c8'
11 | console.log("UserOperation hash:", userOperationHash)
12 |
13 | // let's also wait for the userOperation to be included, by continually querying for the receipts
14 | console.log("Querying for receipts...")
15 | let receipt = null
16 | while (receipt === null) {
17 | receipt = await pimlicoProvider.send("eth_getUserOperationReceipt", [userOperationHash])
18 | console.log(receipt)
19 | await new Promise((r) => setTimeout(r, 1000)) //sleep
20 | }
21 |
22 | const txHash = receipt.receipt.transactionHash
23 | console.log(`${txHash}`)
24 | }
25 |
26 | run()
--------------------------------------------------------------------------------
/contracts/SenderCreator.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: GPL-3.0
2 | pragma solidity ^0.8.12;
3 | import 'hardhat/console.sol';
4 |
5 | /**
6 | * helper contract for EntryPoint, to call userOp.initCode from a "neutral" address,
7 | * which is explicitly not the entryPoint itself.
8 | */
9 | contract SenderCreator {
10 |
11 | /**
12 | * call the "initCode" factory to create and return the sender account address
13 | * @param initCode the initCode value from a UserOp. contains 20 bytes of factory address, followed by calldata
14 | * @return sender the returned address of the created account, or zero address on failure.
15 | */
16 | function createSender(bytes calldata initCode) external returns (address sender) {
17 | address factory = address(bytes20(initCode[0 : 20]));
18 | bytes memory initCallData = initCode[20 :];
19 | bool success;
20 | /* solhint-disable no-inline-assembly */
21 | assembly {
22 | success := call(gas(), factory, 0, add(initCallData, 0x20), mload(initCallData), 0, 32)
23 | sender := mload(0)
24 | }
25 |
26 | if (!success) {
27 | sender = address(0);
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/scripts/fund.js:
--------------------------------------------------------------------------------
1 | const { ethers } = require("ethers")
2 |
3 | const localhost = 'http://127.0.0.1:8545'
4 | const provider = new ethers.providers.JsonRpcProvider(localhost)
5 |
6 | async function fund() {
7 |
8 | // transfer funds to both accounts - this one and the multisig
9 | const hardhatPk = '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80'
10 | const addressOne = '0xEFA78F620881ae23aBD760f1c94B184E9a60117e'
11 | const multisigAddress = '0x027F1b6B5866CA6dB2c46c6b228C059Fbb8B7040'
12 |
13 | const fundWallet = new ethers.Wallet(hardhatPk, provider)
14 | const firstTxn = await fundWallet.sendTransaction({
15 | to: addressOne,
16 | value: ethers.utils.parseEther('20')
17 | })
18 | await firstTxn.wait()
19 | const secondTxn = await fundWallet.sendTransaction({
20 | to: multisigAddress,
21 | value: ethers.utils.parseEther('20')
22 | })
23 | await secondTxn.wait()
24 |
25 | console.log(ethers.utils.formatEther(await provider.getBalance(addressOne)))
26 | console.log(ethers.utils.formatEther(await provider.getBalance(multisigAddress)))
27 | }
28 |
29 | // We recommend this pattern to be able to use async/await everywhere
30 | // and properly handle errors.
31 | fund().catch((error) => {
32 | console.error(error)
33 | process.exitCode = 1
34 | });
35 |
--------------------------------------------------------------------------------
/contracts/EntryPoint.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: GPL-3.0
2 | pragma solidity ^0.8.12;
3 | import 'hardhat/console.sol';
4 | import './SenderCreator.sol';
5 | import './libs/Exec.sol';
6 |
7 | /**
8 | * helper contract for EntryPoint, to call userOp.initCode from a "neutral" address,
9 | * which is explicitly not the entryPoint itself.
10 | */
11 | contract EntryPoint {
12 |
13 | error FailedOp(uint256 opIndex, string reason);
14 |
15 | // create the sender's contract if needed.
16 | function createSenderIfNeeded(address sender, uint256 verificationGasLimit, bytes calldata initCode) public {
17 | if (initCode.length != 0) {
18 | // if (sender.code.length != 0) revert FailedOp(0, "AA10 sender already constructed");
19 | SenderCreator senderCreator = new SenderCreator();
20 | address sender1 = senderCreator.createSender{gas : verificationGasLimit}(initCode);
21 | if (sender1 == address(0)) revert FailedOp(0, "AA13 initCode failed or OOG");
22 | if (sender1 != sender) revert FailedOp(0, "AA14 initCode must return sender");
23 | if (sender1.code.length == 0) revert FailedOp(0, "AA15 initCode must create sender");
24 | }
25 | }
26 |
27 | function sendTxnOutside(address sender, bytes calldata callData, uint256 callGasLimit) public {
28 | this.sendTxn(sender, callData, callGasLimit);
29 | }
30 |
31 |
32 | function sendTxn(address sender, bytes calldata callData, uint256 callGasLimit) external {
33 | Exec.call(sender, 0, callData, callGasLimit);
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/common/QRCodeScanner.js:
--------------------------------------------------------------------------------
1 | import { Html5QrcodeScanner } from "html5-qrcode";
2 | import { useEffect } from "react";
3 |
4 | import './styles.css'
5 |
6 | const qrcodeRegionId = "html5qr-code-full-region";
7 |
8 | // Creates the configuration object for Html5QrcodeScanner.
9 | const createConfig = (props) => {
10 | let config = {};
11 | if (props.fps) {
12 | config.fps = props.fps;
13 | }
14 | if (props.qrbox) {
15 | config.qrbox = props.qrbox;
16 | }
17 | if (props.aspectRatio) {
18 | config.aspectRatio = props.aspectRatio;
19 | }
20 | if (props.disableFlip !== undefined) {
21 | config.disableFlip = props.disableFlip;
22 | }
23 | return config;
24 | };
25 |
26 | const QRCodeScanner = (props) => {
27 | useEffect(() => {
28 | // when component mounts
29 | const config = createConfig(props);
30 | const verbose = props.verbose === true;
31 | // Suceess callback is required.
32 | if (!props.qrCodeSuccessCallback) {
33 | throw "qrCodeSuccessCallback is required callback.";
34 | }
35 | const html5QrcodeScanner = new Html5QrcodeScanner(
36 | qrcodeRegionId,
37 | config,
38 | verbose
39 | );
40 | html5QrcodeScanner.render(
41 | props.qrCodeSuccessCallback,
42 | props.qrCodeErrorCallback
43 | );
44 |
45 | // cleanup function when component will unmount
46 | return () => {
47 | html5QrcodeScanner.clear().catch((error) => {
48 | console.error("Failed to clear html5QrcodeScanner. ", error);
49 | });
50 | };
51 | }, [props]);
52 |
53 | return ;
54 | };
55 |
56 | export default QRCodeScanner;
57 |
--------------------------------------------------------------------------------
/src/multisig/components/JoinMultisig.tsx:
--------------------------------------------------------------------------------
1 | import { Key } from "@borislav.itskov/schnorrkel.js";
2 | import {
3 | Button,
4 | Modal,
5 | ModalContent,
6 | ModalOverlay,
7 | useDisclosure,
8 | } from "@chakra-ui/react";
9 | import { utils } from "ethers";
10 | import { useMemo } from "react";
11 | import QRCode from "react-qr-code";
12 | import { getSchnorrkelInstance } from "../../singletons/Schnorr";
13 | import { useEOA } from "../../auth/context/eoa";
14 |
15 | const JoinMultisig = (props: any) => {
16 | const { eoaPublicKey, eoaPrivateKey } = useEOA()
17 |
18 | const { isOpen, onOpen, onClose } = useDisclosure();
19 | const qrCodeValue = useMemo(() => {
20 | const schnorrkel = getSchnorrkelInstance();
21 |
22 | const privateKey = new Key(Buffer.from(utils.arrayify(eoaPrivateKey)))
23 | const publicNonces = schnorrkel.hasNonces(privateKey)
24 | ? schnorrkel.getPublicNonces(privateKey)
25 | : schnorrkel.generatePublicNonces(privateKey);
26 |
27 | const kOne = publicNonces.kPublic.toHex();
28 | const kTwo = publicNonces.kTwoPublic.toHex();
29 |
30 | return eoaPublicKey + "|" + kOne + "|" + kTwo
31 | }, [eoaPrivateKey, eoaPublicKey]);
32 |
33 | return (
34 | <>
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 | >
43 | );
44 | };
45 |
46 | export default JoinMultisig;
47 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom/client";
3 | import "./index.css";
4 | import App from "./App";
5 | import reportWebVitals from "./reportWebVitals";
6 | import * as serviceWorkerRegistration from "./config/service-worker-registration";
7 | import { MultisigProvider } from "./auth/context/multisig";
8 | import { ChakraProvider, extendTheme } from "@chakra-ui/react";
9 | import { EOAProvider } from "./auth/context/eoa";
10 |
11 | // TODO: Optimize those by importing only the weights we need
12 | import '@fontsource-variable/roboto-slab';
13 | import '@fontsource-variable/open-sans';
14 | import { StepProvider } from "./auth/context/step";
15 |
16 | const theme = extendTheme({
17 | fonts: {
18 | heading: `'Roboto Slab Variable', sans-serif`,
19 | body: `'Open Sans', sans-serif`,
20 | },
21 | })
22 |
23 | const root = ReactDOM.createRoot(
24 | document.getElementById("root") as HTMLElement
25 | );
26 | root.render(
27 | //
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | //
38 | );
39 |
40 | // If you want your app to work offline and load faster, you can change
41 | // unregister() to register() below. Note this comes with some pitfalls.
42 | // Learn more about service workers: https://cra.link/PWA
43 | serviceWorkerRegistration.register();
44 |
45 | // If you want to start measuring performance in your app, pass a function
46 | // to log results (for example: reportWebVitals(console.log))
47 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
48 | reportWebVitals();
49 |
--------------------------------------------------------------------------------
/test/DeployERC4337.test.js:
--------------------------------------------------------------------------------
1 | const { ethers } = require('ethers')
2 | const { AmbireAccountFactory } = require('./config')
3 | const ERC4337Account = require('../artifacts/contracts/ERC4337Account.sol/ERC4337Account.json')
4 |
5 | const salt = '0x0'
6 | function getAddressCreateTwo(factoryAddress, bytecode) {
7 | return ethers.utils.getCreate2Address(factoryAddress, ethers.utils.hexZeroPad(salt, 32), ethers.utils.keccak256(bytecode))
8 | }
9 |
10 | const localhost = 'http://127.0.0.1:8545'
11 | const mainProvider = new ethers.providers.JsonRpcProvider(localhost)
12 |
13 | describe('DeployERC4337 tests', function(){
14 | it('deploy on localhost', async function(){
15 | const pkWithETH = '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80'
16 | const owner = new ethers.Wallet(pkWithETH, mainProvider)
17 | const factory = new ethers.ContractFactory(
18 | AmbireAccountFactory.abi,
19 | AmbireAccountFactory.bytecode,
20 | owner
21 | )
22 | const deployContract = await factory.deploy(owner.address)
23 | const ENTRY_POINT_ADDRESS = "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789"
24 |
25 | const abicoder = new ethers.utils.AbiCoder()
26 | const bytecodeWithArgs = ethers.utils.concat([
27 | ERC4337Account.bytecode,
28 | abicoder.encode(['address', 'address[]'], [ENTRY_POINT_ADDRESS, [owner.address]])
29 | ])
30 |
31 | const erc4337Account = await deployContract.deploy(bytecodeWithArgs, salt)
32 | const receipt = await erc4337Account.wait()
33 | console.log(receipt)
34 |
35 | const addr = getAddressCreateTwo(deployContract.address, bytecodeWithArgs)
36 | const acc = new ethers.Contract(addr, ERC4337Account.abi, owner)
37 | const entryPoint = await acc.entryPoint()
38 | console.log(entryPoint)
39 | })
40 | })
--------------------------------------------------------------------------------
/src/common/styles.css:
--------------------------------------------------------------------------------
1 | /** QR code scanner couldnt be scanner **/
2 |
3 | #html5qr-code-full-region__scan_region {
4 | display: flex;
5 | justify-content: center;
6 | border-radius: 12px;
7 | }
8 |
9 | button.html5-qrcode-element {
10 | border-radius: var(--chakra-radii-md);
11 | font-weight: var(--chakra-fontWeights-semibold);
12 | transition-property: var(--chakra-transition-property-common);
13 | transition-duration: var(--chakra-transition-duration-normal);
14 | height: var(--chakra-sizes-10);
15 | min-width: var(--chakra-sizes-10);
16 | font-size: var(--chakra-fontSizes-md);
17 | -webkit-padding-start: var(--chakra-space-4);
18 | padding-inline-start: var(--chakra-space-4);
19 | -webkit-padding-end: var(--chakra-space-4);
20 | padding-inline-end: var(--chakra-space-4);
21 | background: var(--chakra-colors-green-500);
22 | color: var(--chakra-colors-white);
23 | }
24 |
25 | #html5qr-code-full-region__header_message {
26 | font-weight: var(--chakra-fontWeights-semibold);
27 | }
28 |
29 | select.html5-qrcode-element {
30 | padding-inline-end: var(--chakra-space-8);
31 | display: block;
32 | margin: 10px auto;
33 | outline: transparent solid 2px;
34 | outline-offset: 2px;
35 | position: relative;
36 | transition-property: var(--chakra-transition-property-common);
37 | transition-duration: var(--chakra-transition-duration-normal);
38 | padding-bottom: 1px;
39 | line-height: var(--chakra-lineHeights-normal);
40 | --select-bg: var(--chakra-colors-white);
41 | font-size: var(--chakra-fontSizes-md);
42 | padding-inline-start: var(--chakra-space-4);
43 | height: var(--chakra-sizes-10);
44 | border-radius: var(--chakra-radii-md);
45 | border-width: 1px;
46 | background-color: var(--chakra-colors-blue-100);
47 | border-style: solid;
48 | border-image: initial;
49 | border-color: inherit;
50 | }
51 |
--------------------------------------------------------------------------------
/src/install/InstallPWA.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import { Box, Button } from "@chakra-ui/react";
3 |
4 | interface BeforeInstallPromptEvent extends Event {
5 | readonly platforms: string[];
6 | readonly userChoice: Promise<{
7 | outcome: "accepted" | "dismissed";
8 | platform: string;
9 | }>;
10 | prompt: () => Promise;
11 | }
12 |
13 | const InstallPWA: React.FC = () => {
14 | const [installable, setInstallable] = useState(false);
15 | const [installPromptEvent, setInstallPromptEvent] =
16 | useState(null);
17 |
18 | useEffect(() => {
19 | const listener = (e: BeforeInstallPromptEvent) => {
20 | e.preventDefault();
21 | setInstallable(true);
22 | setInstallPromptEvent(e);
23 | };
24 |
25 | window.addEventListener("beforeinstallprompt", listener as any);
26 |
27 | // Cleanup
28 | return () =>
29 | window.removeEventListener("beforeinstallprompt", listener as any);
30 | }, []);
31 |
32 | const handleClick = async () => {
33 | if (!installPromptEvent) return;
34 |
35 | // Show the install prompt
36 | await installPromptEvent.prompt();
37 |
38 | // Wait for the user to respond to the prompt
39 | const { outcome } = await installPromptEvent.userChoice;
40 | console.log("User response to the install prompt:", outcome);
41 |
42 | // Cleanup
43 | setInstallPromptEvent(null);
44 | setInstallable(false);
45 | };
46 |
47 | return (
48 |
58 |
61 |
62 | );
63 | };
64 |
65 | export default InstallPWA;
66 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | QR Seal
28 |
29 |
30 |
31 |
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/contracts/ERC4337Account.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: agpl-3.0
2 | pragma solidity 0.8.19;
3 |
4 | import "./AmbireAccount.sol";
5 | import "./libs/SignatureValidator.sol";
6 | import "../node_modules/@account-abstraction/contracts/interfaces/IAccount.sol";
7 | import "../node_modules/@account-abstraction/contracts/interfaces/UserOperation.sol";
8 |
9 | contract ERC4337Account is AmbireAccount, IAccount {
10 | address public immutable entryPoint;
11 |
12 | // return value in case of signature failure, with no time-range.
13 | // equivalent to packSigTimeRange(true,0,0);
14 | uint256 constant internal SIG_VALIDATION_FAILED = 1;
15 |
16 | constructor(address _entryPoint, address[] memory addrs) {
17 | entryPoint = _entryPoint;
18 | privileges[_entryPoint] = bytes32(uint(1));
19 |
20 | uint len = addrs.length;
21 | for (uint i=0; i 0) {
43 | (bool success,) = payable(msg.sender).call{value : missingAccountFunds}("");
44 | (success);
45 | // ignore failure (its EntryPoint's job to verify, not account.)
46 | }
47 | }
48 | }
--------------------------------------------------------------------------------
/src/utils/helpers.ts:
--------------------------------------------------------------------------------
1 | import Schnorrkel, { Key } from "@borislav.itskov/schnorrkel.js";
2 | import { ethers } from "ethers";
3 | import ERC4337Account from '../builds/ERC4337Account.json'
4 | import { ENTRY_POINT_ADDRESS, FACTORY_ADDRESS } from "../config/constants";
5 |
6 | const salt = '0x0'
7 | export function getAmbireAccountAddress(factoryAddress: string, bytecode: string|Uint8Array) {
8 | return ethers.utils.getCreate2Address(factoryAddress, ethers.utils.hexZeroPad(salt, 32), ethers.utils.keccak256(bytecode))
9 | }
10 |
11 | export function wrapSchnorr(sig: string) {
12 | return `${sig}${'04'}`
13 | }
14 |
15 | export function computeSchnorrAddress(combinedPublicKey: Key) {
16 | const px = ethers.utils.hexlify(combinedPublicKey.buffer.slice(1, 33));
17 | const schnorrHash = ethers.utils.keccak256(
18 | ethers.utils.solidityPack(["string", "bytes"], ["SCHNORR", px])
19 | );
20 | return "0x" + schnorrHash.slice(schnorrHash.length - 40, schnorrHash.length);
21 | }
22 |
23 | export function getDeployCalldata(bytecodeWithArgs: any) {
24 | const abi = ['function deploy(bytes calldata code, uint256 salt) external']
25 | const iface = new ethers.utils.Interface(abi)
26 | return iface.encodeFunctionData('deploy', [
27 | bytecodeWithArgs,
28 | '0x0'
29 | ])
30 | }
31 |
32 | export function getExecuteCalldata(txns: any) {
33 | const abi = ['function executeBySender(tuple(address, uint, bytes)[] calldata txns) external payable']
34 | const iface = new ethers.utils.Interface(abi)
35 | return iface.encodeFunctionData('executeBySender', txns)
36 | }
37 |
38 | export function getMultisigAddress(publicKeys: Array) {
39 | const combinedPublicKey = Schnorrkel.getCombinedPublicKey(publicKeys);
40 | const schnorrVirtualAddr = computeSchnorrAddress(combinedPublicKey)
41 |
42 | const abiCoder = new ethers.utils.AbiCoder();
43 | const bytecodeWithArgs = ethers.utils.concat([
44 | ERC4337Account.bytecode,
45 | abiCoder.encode(['address', 'address[]'], [ENTRY_POINT_ADDRESS, [schnorrVirtualAddr]])
46 | ])
47 | return getAmbireAccountAddress(FACTORY_ADDRESS, bytecodeWithArgs);
48 | }
--------------------------------------------------------------------------------
/scripts/erc4337/getNonce.js:
--------------------------------------------------------------------------------
1 | const { EntryPoint__factory } = require("@account-abstraction/contracts")
2 | const { StaticJsonRpcProvider } = require("@ethersproject/providers")
3 | const { ethers } = require("ethers")
4 | const { rpcs } = require("./config")
5 | require('dotenv').config();
6 | const ERC4337Account = require('../../artifacts/contracts/ERC4337Account.sol/ERC4337Account.json')
7 | const salt = '0x0'
8 |
9 | function getAmbireAccountAddress(factoryAddress, bytecode) {
10 | return ethers.utils.getCreate2Address(factoryAddress, ethers.utils.hexZeroPad(salt, 32), ethers.utils.keccak256(bytecode))
11 | }
12 |
13 | function getSchnorrAddress(pk) {
14 | const publicKey = ethers.utils.arrayify(ethers.utils.computePublicKey(ethers.utils.arrayify(pk), true))
15 | const px = ethers.utils.hexlify(publicKey.slice(1, 33))
16 | const hash = ethers.utils.keccak256(ethers.utils.solidityPack(['string', 'bytes'], ['SCHNORR', px]))
17 | return '0x' + hash.slice(hash.length - 40, hash.length)
18 | }
19 |
20 | const run = async () => {
21 | const someWallet = ethers.Wallet.createRandom()
22 | const pk = someWallet.privateKey
23 | const AMBIRE_ACCOUNT_FACTORY_ADDR = "0x153E957A9ff1688BbA982856Acf178524aF96D78"
24 | const ENTRY_POINT_ADDRESS = "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789"
25 | const provider = new StaticJsonRpcProvider(rpcs.mumbai)
26 | const entryPoint = EntryPoint__factory.connect(ENTRY_POINT_ADDRESS, provider)
27 | const owner = new ethers.Wallet(pk, provider)
28 | const abicoder = new ethers.utils.AbiCoder()
29 | const schnorrVirtualAddr = getSchnorrAddress(pk)
30 | const bytecodeWithArgs = ethers.utils.concat([
31 | ERC4337Account.bytecode,
32 | abicoder.encode(['address', 'address[]'], [ENTRY_POINT_ADDRESS, [owner.address, schnorrVirtualAddr]])
33 | ])
34 |
35 | const senderAddress = getAmbireAccountAddress(AMBIRE_ACCOUNT_FACTORY_ADDR, bytecodeWithArgs)
36 | const senderNonce = await entryPoint.getNonce(senderAddress, 0)
37 | console.log(senderNonce)
38 |
39 | const alreadyExistingNonce = await entryPoint.getNonce('0x98c7Ff1979781058798aC2824325c7e89f541f8b', 0)
40 | console.log(alreadyExistingNonce)
41 | }
42 |
43 | run()
--------------------------------------------------------------------------------
/src/common/accounts/AccountAddress.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Box, Text, Flex, Button } from "@chakra-ui/react";
3 | import Blockies from "react-blockies";
4 | import CreateMultisigByScanning from "../../multisig/components/CreateMultisigByScanning";
5 |
6 | const AccountAddress = ({
7 | address,
8 | addressType,
9 | onCreate,
10 | type,
11 | }: any) => {
12 | return (
13 |
14 | {address ? (
15 |
16 |
17 |
18 | {address ? (
19 |
25 | ) : (
26 | <>>
27 | )}
28 |
29 |
36 | {addressType}
37 |
38 |
39 |
40 |
48 | {address || "No address generated yet"}
49 |
50 |
51 |
52 | ) : type === "eoa" ? (
53 |
54 |
64 |
65 | ) : (
66 |
67 | )}
68 |
69 | );
70 | };
71 |
72 | export default AccountAddress;
73 |
--------------------------------------------------------------------------------
/contracts/libs/Exec.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: LGPL-3.0-only
2 | pragma solidity >=0.7.5 <0.9.0;
3 |
4 | // solhint-disable no-inline-assembly
5 |
6 | /**
7 | * Utility functions helpful when making different kinds of contract calls in Solidity.
8 | */
9 | library Exec {
10 |
11 | function call(
12 | address to,
13 | uint256 value,
14 | bytes memory data,
15 | uint256 txGas
16 | ) internal returns (bool success) {
17 | assembly {
18 | success := call(txGas, to, value, add(data, 0x20), mload(data), 0, 0)
19 | }
20 | }
21 |
22 | function staticcall(
23 | address to,
24 | bytes memory data,
25 | uint256 txGas
26 | ) internal view returns (bool success) {
27 | assembly {
28 | success := staticcall(txGas, to, add(data, 0x20), mload(data), 0, 0)
29 | }
30 | }
31 |
32 | function delegateCall(
33 | address to,
34 | bytes memory data,
35 | uint256 txGas
36 | ) internal returns (bool success) {
37 | assembly {
38 | success := delegatecall(txGas, to, add(data, 0x20), mload(data), 0, 0)
39 | }
40 | }
41 |
42 | // get returned data from last call or calldelegate
43 | function getReturnData(uint256 maxLen) internal pure returns (bytes memory returnData) {
44 | assembly {
45 | let len := returndatasize()
46 | if gt(len, maxLen) {
47 | len := maxLen
48 | }
49 | let ptr := mload(0x40)
50 | mstore(0x40, add(ptr, add(len, 0x20)))
51 | mstore(ptr, len)
52 | returndatacopy(add(ptr, 0x20), 0, len)
53 | returnData := ptr
54 | }
55 | }
56 |
57 | // revert with explicit byte array (probably reverted info from call)
58 | function revertWithData(bytes memory returnData) internal pure {
59 | assembly {
60 | revert(add(returnData, 32), mload(returnData))
61 | }
62 | }
63 |
64 | function callAndRevert(address to, bytes memory data, uint256 maxLen) internal {
65 | bool success = call(to,0,data,gasleft());
66 | if (!success) {
67 | revertWithData(getReturnData(maxLen));
68 | }
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/src/auth/context/multisig.tsx:
--------------------------------------------------------------------------------
1 | import { Key } from "@borislav.itskov/schnorrkel.js";
2 | import { createContext, useState, useMemo, useCallback } from "react";
3 |
4 | const STORAGE_KEY_MULTISIG = "multisig-data";
5 |
6 | export interface MultisigData {
7 | multisigPartnerPublicKey: string;
8 | multisigPartnerKPublicHex: string;
9 | multisigPartnerKTwoPublicHex: string;
10 | multisigAddr: string;
11 | multisigPartnerSignature?: string;
12 | combinedPublicKey?: Key;
13 | }
14 |
15 | export const getMultisigData = () =>
16 | localStorage.getItem(STORAGE_KEY_MULTISIG) || "";
17 |
18 | export const createAndStoreMultisigDataInLocalStorageIfNeeded = (multisigData: MultisigData) => {
19 | const storedMultisigData = getMultisigData();
20 | if (storedMultisigData) return;
21 | // Store the private key hex string in local storage
22 | localStorage.setItem(STORAGE_KEY_MULTISIG, JSON.stringify(multisigData));
23 | };
24 |
25 | export const getMultisigDataFromLocalStorage = () => {
26 | const multisigData = getMultisigData();
27 | if (!multisigData) return "";
28 |
29 | return JSON.parse(multisigData)
30 | };
31 |
32 |
33 | const MultisigContext = createContext({
34 | multisigData: getMultisigDataFromLocalStorage(),
35 | createAndStoreMultisigDataIfNeeded: (data: any) => {},
36 | getAllMultisigData: () => getMultisigDataFromLocalStorage(),
37 | });
38 |
39 | export const MultisigProvider = ({ children }: any) => {
40 | const [multisigData, setMultisigData] = useState(getMultisigDataFromLocalStorage());
41 |
42 | const getAllMultisigData = useCallback(() => {
43 | if (!multisigData) {
44 | return getMultisigDataFromLocalStorage()
45 | }
46 | else return multisigData
47 | }, [multisigData])
48 |
49 | const createAndStoreMultisigDataIfNeeded = (multisigData: MultisigData) => {
50 | createAndStoreMultisigDataInLocalStorageIfNeeded(multisigData)
51 | setMultisigData(multisigData)
52 | }
53 |
54 | return (
55 | ({
57 | multisigData,
58 | createAndStoreMultisigDataIfNeeded,
59 | getAllMultisigData,
60 | }), [multisigData, getAllMultisigData])}
61 | >
62 | {children}
63 |
64 | );
65 | };
66 |
67 | export default MultisigContext;
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
--------------------------------------------------------------------------------
/src/auth/context/eoa.tsx:
--------------------------------------------------------------------------------
1 | import React, { createContext, useContext, useState, useEffect, useCallback } from 'react';
2 | import { utils } from 'ethers';
3 | import { useSteps } from './step';
4 | import { useToast } from '@chakra-ui/react';
5 |
6 | const STORAGE_KEY_EOA = 'eoa-private-key';
7 |
8 | type EOAContextType = {
9 | eoaPrivateKey: string;
10 | eoaPublicKey: string;
11 | eoaAddress: string;
12 | createAndStoreEOA: () => void;
13 | };
14 |
15 | const EOAContext = createContext({
16 | eoaPrivateKey: '',
17 | eoaPublicKey: '',
18 | eoaAddress: '',
19 | createAndStoreEOA: () => {},
20 | });
21 |
22 | export const useEOA = () => useContext(EOAContext);
23 |
24 | export const EOAProvider: React.FC = ({ children }) => {
25 | const toast = useToast();
26 | const { setActiveStep } = useSteps()
27 | const [eoaPrivateKey, setEOAPrivateKey] = useState('');
28 | const [eoaPublicKey, setEOAPublicKey] = useState('');
29 | const [eoaAddress, setEOAAddress] = useState('');
30 |
31 | useEffect(() => {
32 | const storedPrivateKey = localStorage.getItem(STORAGE_KEY_EOA) || '';
33 | setEOAPrivateKey(storedPrivateKey);
34 | }, []);
35 |
36 | const createAndStoreEOA = useCallback(() => {
37 | const privateKeyBytes = utils.randomBytes(32);
38 | const privateKeyHex = utils.hexlify(privateKeyBytes);
39 | localStorage.setItem(STORAGE_KEY_EOA, privateKeyHex);
40 | setEOAPrivateKey(privateKeyHex);
41 | setActiveStep(1)
42 | toast({
43 | title: 'EOA created!',
44 | description: 'You can now create a multisig wallet.',
45 | status: 'success',
46 | duration: 5000,
47 | isClosable: true,
48 | position: 'top'
49 | });
50 | }, [setActiveStep, toast]);
51 |
52 | useEffect(() => {
53 | if (eoaPrivateKey) {
54 | const privateKeyBytes = utils.arrayify(eoaPrivateKey);
55 | const uncompressedPublicKey = utils.computePublicKey(privateKeyBytes, false);
56 | const publicKey = utils.computePublicKey(uncompressedPublicKey, true);
57 | setEOAPublicKey(publicKey);
58 | setEOAAddress(utils.computeAddress(publicKey));
59 | }
60 | }, [eoaPrivateKey]);
61 |
62 | return (
63 |
64 | {children}
65 |
66 | );
67 | };
68 |
--------------------------------------------------------------------------------
/scripts/deploy.js:
--------------------------------------------------------------------------------
1 | const { ethers } = require("ethers")
2 | const { AmbireAccountFactory, AmbireAccount } = require("../test/config");
3 | const { rpcs, chainIds } = require("./erc4337/config");
4 | require('dotenv').config();
5 | const provider = new ethers.providers.JsonRpcProvider(rpcs.polygon)
6 |
7 | async function generateFactoryDeploy (gasPrice) {
8 | const txn = {}
9 | const hardhatPk = process.env.DEPLOY_PRIVATE_KEY
10 | const fundWallet = new ethers.Wallet(hardhatPk, provider)
11 | const factory = new ethers.ContractFactory(AmbireAccountFactory.abi, AmbireAccountFactory.bytecode, fundWallet)
12 |
13 | txn.data = factory.getDeployTransaction(fundWallet.address)
14 | txn.from = fundWallet.address
15 | txn.value = '0x00'
16 | txn.type = null
17 | txn.gasLimit = ethers.BigNumber.from(1e6)
18 | txn.data = txn.data.data
19 | txn.gasPrice = gasPrice
20 | txn.nonce = await provider.getTransactionCount(fundWallet.address)
21 | txn.chainId = chainIds.polygon
22 | return await fundWallet.signTransaction(txn)
23 | }
24 |
25 | async function generateAmbireDeploy (gasPrice) {
26 | const txn = {}
27 | const hardhatPk = process.env.DEPLOY_PRIVATE_KEY
28 | const fundWallet = new ethers.Wallet(hardhatPk, provider)
29 | const factory = new ethers.ContractFactory(AmbireAccount.abi, AmbireAccount.bytecode, fundWallet)
30 |
31 | txn.data = factory.getDeployTransaction()
32 | txn.from = fundWallet.address
33 | txn.value = '0x00'
34 | txn.type = null
35 | txn.gasLimit = 10000000n
36 | txn.data = txn.data.data
37 | txn.gasPrice = gasPrice
38 | txn.nonce = await provider.getTransactionCount(fundWallet.address)
39 | txn.chainId = chainIds.polygon
40 | return await fundWallet.signTransaction(txn)
41 | }
42 |
43 | async function deploy() {
44 |
45 | const feeData = await provider.getFeeData()
46 | const sig = await generateFactoryDeploy(feeData.gasPrice)
47 | console.log(sig)
48 |
49 | // const contractFactoryAmbire = new ethers.ContractFactory(AmbireAccount.abi, AmbireAccount.bytecode, fundWallet)
50 | // const ambireTxn = contractFactoryAmbire.getDeployTransaction({
51 | // gasPrice: feeData.gasPrice,
52 | // gasLimit: ethers.BigNumber.from(1e6)
53 | // })
54 | // const ambireSig = await fundWallet.signTransaction(ambireTxn)
55 | // console.log(ambireSig)
56 | }
57 |
58 | // We recommend this pattern to be able to use async/await everywhere
59 | // and properly handle errors.
60 | deploy().catch((error) => {
61 | console.error(error)
62 | process.exitCode = 1
63 | });
64 |
--------------------------------------------------------------------------------
/src/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # QR Seal: Multisig Wallet 🦭
2 |
3 | QR Seal is a privacy-preserving, gas-optimized mobile multisig wallet, implemented via account abstraction, ERC-4337 and Schnorr signatures. Read the full description [here](https://devfolio.co/projects/qr-seal-7871).
4 |
5 | A hack for the [ETH Prague 2023 Hackathon](https://ethprague.com/)!
6 |
7 | 
8 |
9 | ## Available Scripts
10 |
11 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
12 |
13 | In the project directory, you can run:
14 |
15 | ### `npm start`
16 |
17 | Runs the app in the development mode.\
18 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
19 |
20 | The page will reload if you make edits.\
21 | You will also see any lint errors in the console.
22 |
23 | ### `npm test`
24 |
25 | Launches the test runner in the interactive watch mode.\
26 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
27 |
28 | ### `npm run build`
29 |
30 | Builds the app for production to the `build` folder.\
31 | It correctly bundles React in production mode and optimizes the build for the best performance.
32 |
33 | The build is minified and the filenames include the hashes.\
34 | Your app is ready to be deployed!
35 |
36 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
37 |
38 | ### `npm run eject`
39 |
40 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!**
41 |
42 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
43 |
44 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
45 |
46 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
47 |
48 | ## Learn More
49 |
50 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
51 |
52 | To learn React, check out the [React documentation](https://reactjs.org/).
53 |
--------------------------------------------------------------------------------
/contracts/AmbireAccountFactory.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: agpl-3.0
2 | pragma solidity 0.8.19;
3 |
4 | import './AmbireAccount.sol';
5 |
6 | contract AmbireAccountFactory {
7 | event LogDeployed(address addr, uint256 salt);
8 |
9 | address public immutable allowedToDrain;
10 |
11 | constructor(address allowed) {
12 | allowedToDrain = allowed;
13 | }
14 |
15 | // @notice allows anyone to deploy any contracft with a specific code/salt
16 | // this is safe because it's CREATE2 deployment
17 | function deploy(bytes calldata code, uint256 salt) external returns (address) {
18 | return deploySafe(code, salt);
19 | }
20 |
21 | // @notice when the relayer needs to act upon an /identity/:addr/submit call, it'll either call execute on the AmbireAccount directly
22 | // if it's already deployed, or call `deployAndExecute` if the account is still counterfactual
23 | // we can't have deployAndExecuteBySender, because the sender will be the factory
24 | function deployAndExecute(
25 | bytes calldata code,
26 | uint256 salt,
27 | AmbireAccount.Transaction[] calldata txns,
28 | bytes calldata signature
29 | ) external {
30 | address payable addr = payable(deploySafe(code, salt));
31 | AmbireAccount(addr).execute(txns, signature);
32 | }
33 |
34 | // @notice This method can be used to withdraw stuck tokens or airdrops
35 | function call(address to, uint256 value, bytes calldata data, uint256 gas) external {
36 | require(msg.sender == allowedToDrain, 'ONLY_AUTHORIZED');
37 | (bool success, bytes memory err) = to.call{ gas: gas, value: value }(data);
38 | require(success, string(err));
39 | }
40 |
41 | // @dev This is done to mitigate possible frontruns where, for example, deploying the same code/salt via deploy()
42 | // would make a pending deployAndExecute fail
43 | // The way we mitigate that is by checking if the contract is already deployed and if so, we continue execution
44 | function deploySafe(bytes memory code, uint256 salt) internal returns (address) {
45 | address expectedAddr = address(
46 | uint160(uint256(keccak256(abi.encodePacked(bytes1(0xff), address(this), salt, keccak256(code)))))
47 | );
48 | uint256 size;
49 | assembly {
50 | size := extcodesize(expectedAddr)
51 | }
52 | // If there is code at that address, we can assume it's the one we were about to deploy,
53 | // because of how CREATE2 and keccak256 works
54 | if (size == 0) {
55 | address addr;
56 | assembly {
57 | addr := create2(0, add(code, 0x20), mload(code), salt)
58 | }
59 | require(addr != address(0), 'FAILED_DEPLOYING');
60 | require(addr == expectedAddr, 'FAILED_MATCH');
61 | emit LogDeployed(addr, salt);
62 | }
63 | return expectedAddr;
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "qr-seal",
3 | "version": "1.0.0",
4 | "private": true,
5 | "dependencies": {
6 | "@borislav.itskov/schnorrkel.js": "^2.0.83",
7 | "@chakra-ui/react": "^2.7.0",
8 | "@craco/craco": "^7.1.0",
9 | "@emotion/react": "^11.11.1",
10 | "@emotion/styled": "^11.11.0",
11 | "@fontsource-variable/open-sans": "^5.0.4",
12 | "@fontsource-variable/roboto-slab": "^5.0.3",
13 | "@testing-library/jest-dom": "^5.16.5",
14 | "@testing-library/react": "^13.4.0",
15 | "@testing-library/user-event": "^13.5.0",
16 | "@types/jest": "^27.5.2",
17 | "@types/node": "^16.18.34",
18 | "@types/react": "^18.2.9",
19 | "@types/react-dom": "^18.2.4",
20 | "assert": "^2.0.0",
21 | "buffer": "^6.0.3",
22 | "crypto-browserify": "^3.12.0",
23 | "ethereumjs-abi": "^0.6.8",
24 | "ethers": "^5.7.2",
25 | "framer-motion": "^10.12.16",
26 | "html5-qrcode": "^2.3.8",
27 | "js-sha3": "^0.8.0",
28 | "react": "^18.2.0",
29 | "react-blockies": "^1.4.1",
30 | "react-dom": "^18.2.0",
31 | "react-hook-form": "^7.44.3",
32 | "react-qr-code": "^2.0.11",
33 | "react-scripts": "5.0.1",
34 | "stream-browserify": "^3.0.0",
35 | "typescript": "^4.9.5",
36 | "web-vitals": "^2.1.4",
37 | "workbox-background-sync": "^6.6.0",
38 | "workbox-broadcast-update": "^6.6.0",
39 | "workbox-cacheable-response": "^6.6.0",
40 | "workbox-core": "^6.6.0",
41 | "workbox-expiration": "^6.6.0",
42 | "workbox-google-analytics": "^6.6.0",
43 | "workbox-navigation-preload": "^6.6.0",
44 | "workbox-precaching": "^6.6.0",
45 | "workbox-range-requests": "^6.6.0",
46 | "workbox-routing": "^6.6.0",
47 | "workbox-strategies": "^6.6.0",
48 | "workbox-streams": "^6.6.0"
49 | },
50 | "scripts": {
51 | "start": "craco start",
52 | "build": "craco build",
53 | "test": "craco test",
54 | "eject": "craco eject"
55 | },
56 | "eslintConfig": {
57 | "extends": [
58 | "react-app",
59 | "react-app/jest"
60 | ]
61 | },
62 | "browserslist": {
63 | "production": [
64 | ">0.2%",
65 | "not dead",
66 | "not op_mini all"
67 | ],
68 | "development": [
69 | "last 1 chrome version",
70 | "last 1 firefox version",
71 | "last 1 safari version"
72 | ]
73 | },
74 | "devDependencies": {
75 | "@account-abstraction/contracts": "^0.6.0",
76 | "@nomicfoundation/hardhat-toolbox": "^2.0.2",
77 | "@types/chai": "^4.3.5",
78 | "@types/mocha": "^10.0.1",
79 | "@types/node": "^20.2.5",
80 | "@types/react-blockies": "^1.4.1",
81 | "chai": "^4.3.7",
82 | "hardhat": "^2.15.0",
83 | "ts-node": "^10.9.1"
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/src/deploy/getBytecodeJs.js:
--------------------------------------------------------------------------------
1 | const abi = require('ethereumjs-abi')
2 | const keccak256 = require('js-sha3').keccak256
3 |
4 | function evmPush(data) {
5 | if (data.length < 1) throw new Error('evmPush: no data')
6 | if (data.length > 32) throw new Error('evmPush: data too long')
7 | const opCode = data.length + 95
8 | const opCodeBuf = Buffer.alloc(1)
9 | opCodeBuf.writeUInt8(opCode, 0)
10 | return Buffer.concat([opCodeBuf, data])
11 | }
12 |
13 | function sstoreCode(slotNumber, keyType, key, valueType, valueBuf) {
14 | const buf = abi.rawEncode([keyType, valueType], [key, slotNumber])
15 | const slot = keccak256(buf)
16 | return Buffer.concat([
17 | evmPush(typeof valueBuf === 'string' ? Buffer.from(valueBuf.slice(2), 'hex') : valueBuf),
18 | evmPush(Buffer.from(slot, 'hex')),
19 | Buffer.from('55', 'hex')
20 | ])
21 | }
22 |
23 | function getProxyDeployBytecode(masterContractAddr, privLevels, opts = { privSlot: 0 }) {
24 | const { privSlot = 0 } = opts
25 | if (privLevels.length > 3) throw new Error('getProxyDeployBytecode: max 3 privLevels')
26 | const storage = Buffer.concat(
27 | privLevels.map(({addr, hash}) => {
28 | return hash !== true
29 | ? sstoreCode(privSlot, 'address', addr, 'bytes32', hash)
30 | : sstoreCode(privSlot, 'address', addr, 'bool', Buffer.from('01', 'hex'))
31 | })
32 | )
33 | const initial = Buffer.from('3d602d80', 'hex')
34 | // NOTE: this means we can't support offset>256
35 | // @TODO solve this case; this will remove the "max 3 privLevels" restriction
36 | const offset = storage.length + initial.length + 6 // 6 more bytes including the push added later on
37 | if (offset > 256) throw new Error('getProxyDeployBytecode: internal: offset>256')
38 | const initialCode = Buffer.concat([storage, initial, evmPush(Buffer.from([offset]))])
39 | const masterAddrBuf = Buffer.from(masterContractAddr.slice(2).replace(/^(00)+/, ''), 'hex')
40 |
41 | // TO DO: check if masterAddrBuf.length actually makes sense
42 | if (masterAddrBuf.length > 20) throw new Error('invalid address')
43 | return `0x${initialCode.toString('hex')}3d3981f3363d3d373d3d3d363d${evmPush(
44 | masterAddrBuf
45 | ).toString('hex')}5af43d82803e903d91602b57fd5bf3`
46 | }
47 |
48 | function getStorageSlotsFromArtifact(buildInfo) {
49 | if (!buildInfo) return { privSlot: 0}
50 | const ambireAccountArtifact = buildInfo.output.sources['contracts/AmbireAccount.sol']
51 | const identityNode = ambireAccountArtifact.ast.nodes.find(
52 | (el) => el.nodeType === 'ContractDefinition' && el.name === 'AmbireAccount'
53 | )
54 | const storageVariableNodes = identityNode.nodes.filter(
55 | (n) => n.nodeType === 'VariableDeclaration' && !n.constant && n.stateVariable
56 | )
57 | const privSlot = storageVariableNodes.findIndex((x) => x.name === 'privileges')
58 |
59 | return { privSlot }
60 | }
61 |
62 | module.exports = {
63 | getProxyDeployBytecode,
64 | getStorageSlotsFromArtifact
65 | }
--------------------------------------------------------------------------------
/src/deploy/getBytecode.ts:
--------------------------------------------------------------------------------
1 | const abi = require('ethereumjs-abi')
2 | const keccak256 = require('js-sha3').keccak256
3 |
4 | function evmPush(data: any) {
5 | if (data.length < 1) throw new Error('evmPush: no data')
6 | if (data.length > 32) throw new Error('evmPush: data too long')
7 | const opCode = data.length + 95
8 | const opCodeBuf = Buffer.alloc(1)
9 | opCodeBuf.writeUInt8(opCode, 0)
10 | return Buffer.concat([opCodeBuf, data])
11 | }
12 |
13 | function sstoreCode(slotNumber: any, keyType: any, key: any, valueType: any, valueBuf: any) {
14 | const buf = abi.rawEncode([keyType, valueType], [key, slotNumber])
15 | const slot = keccak256(buf)
16 | return Buffer.concat([
17 | evmPush(typeof valueBuf === 'string' ? Buffer.from(valueBuf.slice(2), 'hex') : valueBuf),
18 | evmPush(Buffer.from(slot, 'hex')),
19 | Buffer.from('55', 'hex')
20 | ])
21 | }
22 |
23 | export interface PrivLevels {
24 | addr: string,
25 | hash: string | boolean
26 | }
27 |
28 | export function getProxyDeployBytecode(masterContractAddr: string, privLevels: PrivLevels[], opts = { privSlot: 0 }) {
29 | const { privSlot = 0 } = opts
30 | if (privLevels.length > 3) throw new Error('getProxyDeployBytecode: max 3 privLevels')
31 | const storage = Buffer.concat(
32 | privLevels.map(({addr, hash}) => {
33 | return hash !== true
34 | ? sstoreCode(privSlot, 'address', addr, 'bytes32', hash)
35 | : sstoreCode(privSlot, 'address', addr, 'bool', Buffer.from('01', 'hex'))
36 | })
37 | )
38 | const initial = Buffer.from('3d602d80', 'hex')
39 | // NOTE: this means we can't support offset>256
40 | // @TODO solve this case; this will remove the "max 3 privLevels" restriction
41 | const offset = storage.length + initial.length + 6 // 6 more bytes including the push added later on
42 | if (offset > 256) throw new Error('getProxyDeployBytecode: internal: offset>256')
43 | const initialCode = Buffer.concat([storage, initial, evmPush(Buffer.from([offset]))])
44 | const masterAddrBuf = Buffer.from(masterContractAddr.slice(2).replace(/^(00)+/, ''), 'hex')
45 |
46 | // TO DO: check if masterAddrBuf.length actually makes sense
47 | if (masterAddrBuf.length > 20) throw new Error('invalid address')
48 | return `0x${initialCode.toString('hex')}3d3981f3363d3d373d3d3d363d${evmPush(
49 | masterAddrBuf
50 | ).toString('hex')}5af43d82803e903d91602b57fd5bf3`
51 | }
52 |
53 | export function getStorageSlotsFromArtifact(buildInfo: any) {
54 | if (!buildInfo) return { privSlot: 0}
55 | const ambireAccountArtifact = buildInfo.output.sources['contracts/AmbireAccount.sol']
56 | const identityNode = ambireAccountArtifact.ast.nodes.find(
57 | (el: any) => el.nodeType === 'ContractDefinition' && el.name === 'AmbireAccount'
58 | )
59 | const storageVariableNodes = identityNode.nodes.filter(
60 | (n: any) => n.nodeType === 'VariableDeclaration' && !n.constant && n.stateVariable
61 | )
62 | const privSlot = storageVariableNodes.findIndex((x: any) => x.name === 'privileges')
63 |
64 | return { privSlot }
65 | }
--------------------------------------------------------------------------------
/src/config/service-worker.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-restricted-globals */
2 |
3 | // This service worker can be customized!
4 | // See https://developers.google.com/web/tools/workbox/modules
5 | // for the list of available Workbox modules, or add any other
6 | // code you'd like.
7 | // You can also remove this file if you'd prefer not to use a
8 | // service worker, and the Workbox build step will be skipped.
9 |
10 | import { clientsClaim } from "workbox-core";
11 | import { ExpirationPlugin } from "workbox-expiration";
12 | import { precacheAndRoute, createHandlerBoundToURL } from "workbox-precaching";
13 | import { registerRoute } from "workbox-routing";
14 | import { StaleWhileRevalidate } from "workbox-strategies";
15 |
16 | clientsClaim();
17 |
18 | // Precache all of the assets generated by your build process.
19 | // Their URLs are injected into the manifest variable below.
20 | // This variable must be present somewhere in your service worker file,
21 | // even if you decide not to use precaching. See https://cra.link/PWA
22 | precacheAndRoute(self.__WB_MANIFEST);
23 |
24 | // Set up App Shell-style routing, so that all navigation requests
25 | // are fulfilled with your index.html shell. Learn more at
26 | // https://developers.google.com/web/fundamentals/architecture/app-shell
27 | const fileExtensionRegexp = new RegExp("/[^/?]+\\.[^/]+$");
28 | registerRoute(
29 | // Return false to exempt requests from being fulfilled by index.html.
30 | ({ request, url }) => {
31 | // If this isn't a navigation, skip.
32 | if (request.mode !== "navigate") {
33 | return false;
34 | } // If this is a URL that starts with /_, skip.
35 |
36 | if (url.pathname.startsWith("/_")) {
37 | return false;
38 | } // If this looks like a URL for a resource, because it contains // a file extension, skip.
39 |
40 | if (url.pathname.match(fileExtensionRegexp)) {
41 | return false;
42 | } // Return true to signal that we want to use the handler.
43 |
44 | return true;
45 | },
46 | createHandlerBoundToURL(process.env.PUBLIC_URL + "/index.html")
47 | );
48 |
49 | // An example runtime caching route for requests that aren't handled by the
50 | // precache, in this case same-origin .png requests like those from in public/
51 | registerRoute(
52 | // Add in any other file extensions or routing criteria as needed.
53 | ({ url }) =>
54 | url.origin === self.location.origin && url.pathname.endsWith(".png"), // Customize this strategy as needed, e.g., by changing to CacheFirst.
55 | new StaleWhileRevalidate({
56 | cacheName: "images",
57 | plugins: [
58 | // Ensure that once this runtime cache reaches a maximum size the
59 | // least-recently used images are removed.
60 | new ExpirationPlugin({ maxEntries: 50 }),
61 | ],
62 | })
63 | );
64 |
65 | // This allows the web app to trigger skipWaiting via
66 | // registration.waiting.postMessage({type: 'SKIP_WAITING'})
67 | self.addEventListener("message", (event) => {
68 | if (event.data && event.data.type === "SKIP_WAITING") {
69 | self.skipWaiting();
70 | }
71 | });
72 |
73 | // Any other custom service worker logic can go here.
74 |
--------------------------------------------------------------------------------
/src/multisig/components/CreateMultisigByScanning.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Flex,
3 | Button,
4 | Modal,
5 | ModalContent,
6 | ModalOverlay,
7 | useDisclosure,
8 | useToast,
9 | } from "@chakra-ui/react";
10 | import QRCodeScanner from "../../common/QRCodeScanner";
11 | import { useContext } from "react";
12 | import { Key } from "@borislav.itskov/schnorrkel.js";
13 | import { getMultisigAddress } from "../../utils/helpers";
14 | import { ethers } from "ethers";
15 | import MultisigContext from "../../auth/context/multisig";
16 | import { useEOA } from "../../auth/context/eoa";
17 | import { useSteps } from "../../auth/context/step";
18 |
19 | const CreateMultisigByScanning = (props: any) => {
20 | const toast = useToast()
21 | const { setActiveStep } = useSteps()
22 | const { eoaPublicKey } = useEOA()
23 | const { multisigData, createAndStoreMultisigDataIfNeeded } = useContext(MultisigContext)
24 | const { isOpen, onOpen, onClose } = useDisclosure();
25 |
26 | const handleScanSuccess = (scan: any = "") => {
27 | const data = scan.split("|");
28 |
29 | // TODO: Validate better if data is multisig!
30 | if (data.length !== 3) {
31 | alert("Missing all multisig data in the QR code you scanned!");
32 |
33 | return;
34 | }
35 |
36 | const publicKey = eoaPublicKey;
37 | const multisigPartnerPublicKey = data[0];
38 | const multisigPartnerKPublicHex = data[1];
39 | const multisigPartnerKTwoPublicHex = data[2];
40 |
41 | onClose();
42 |
43 | const publicKeyOne = new Key(Buffer.from(ethers.utils.arrayify(publicKey)));
44 | const publicKeyTwo = new Key(
45 | Buffer.from(ethers.utils.arrayify(multisigPartnerPublicKey))
46 | );
47 |
48 | try {
49 | const multisigAddr = getMultisigAddress([publicKeyOne, publicKeyTwo])
50 |
51 | // Set data in local storage
52 | createAndStoreMultisigDataIfNeeded({
53 | multisigPartnerPublicKey,
54 | multisigPartnerKPublicHex,
55 | multisigPartnerKTwoPublicHex,
56 | multisigAddr
57 | });
58 |
59 | setActiveStep(2)
60 | toast({
61 | title: "Multisig created.",
62 | description: "You can now create a transaction.",
63 | status: "success",
64 | duration: 9000,
65 | isClosable: true,
66 | position: 'top'
67 | })
68 | } catch (e) {
69 | console.log("The multisig creation failed", e);
70 | }
71 | };
72 | const handleScanError = (error: any) => console.error(error);
73 |
74 | if (!eoaPublicKey) return null
75 |
76 | return (
77 | <>
78 | {!multisigData && (
79 |
80 | )}
81 |
82 |
83 |
84 |
88 |
89 |
90 | >
91 | );
92 | };
93 |
94 | export default CreateMultisigByScanning;
95 |
--------------------------------------------------------------------------------
/scripts/deployAndExecute.js:
--------------------------------------------------------------------------------
1 | const { ethers } = require('ethers')
2 | const { expect } = require('chai')
3 | const { getProxyDeployBytecode, getStorageSlotsFromArtifact } = require('../src/deploy/getBytecodeJs')
4 | const { buildInfo, AmbireAccount, AmbireAccountFactory } = require('../test/config')
5 | const { default: Schnorrkel } = require('@borislav.itskov/schnorrkel.js')
6 | const { generateRandomKeys } = require('@borislav.itskov/schnorrkel.js/dist/core/index.js')
7 | const schnorrkel = new Schnorrkel()
8 |
9 | const salt = '0x0'
10 | function getAmbireAccountAddress(factoryAddress, bytecode) {
11 | return ethers.utils.getCreate2Address(factoryAddress, ethers.utils.hexZeroPad(salt, 32), ethers.utils.keccak256(bytecode))
12 | }
13 | function wrapSchnorr(sig) {
14 | return `${sig}${'04'}`
15 | }
16 |
17 | const FACTORY_ADDRESS = "0x1bb0684486c35e35D56FaA806e12f6819dbe9527";
18 | const AMBIRE_ADDRESS = "0x1100E4Cf3fe64b928cccE36c78ad6b7696d72446";
19 | const localhost = 'http://127.0.0.1:8545'
20 | const mainProvider = new ethers.providers.JsonRpcProvider(localhost)
21 |
22 | async function deployAndExecute() {
23 | const pkWithETH = '0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d'
24 | const otherSigner = new ethers.Wallet(pkWithETH, mainProvider)
25 | const factory = new ethers.Contract(FACTORY_ADDRESS, AmbireAccountFactory.abi, otherSigner)
26 |
27 | // configure the schnorr virtual address
28 | const pkPairOne = generateRandomKeys()
29 | const pkPairTwo = generateRandomKeys()
30 | const combinedPublicKey = Schnorrkel.getCombinedPublicKey([
31 | pkPairOne.publicKey,
32 | pkPairTwo.publicKey
33 | ])
34 | const px = ethers.utils.hexlify(combinedPublicKey.buffer.slice(1, 33))
35 | const parity = combinedPublicKey.buffer[0] - 2 + 27
36 | const hash = ethers.utils.keccak256(ethers.utils.solidityPack(['string', 'bytes'], ['SCHNORR', px]))
37 | const combinedPublicAddress = '0x' + hash.slice(hash.length - 40, hash.length)
38 |
39 | // deploy the ambire account
40 | const bytecode = getProxyDeployBytecode(AMBIRE_ADDRESS, [{addr: combinedPublicAddress, hash: true}], {
41 | ...getStorageSlotsFromArtifact(buildInfo)
42 | })
43 | const abiCoder = new ethers.utils.AbiCoder()
44 | const sendTosignerTxn = [otherSigner.address, ethers.utils.parseEther('2'), '0x00']
45 | const txns = [sendTosignerTxn]
46 |
47 | const ambireAccountAddress = getAmbireAccountAddress(factory.address, bytecode)
48 |
49 | // send money to the signer txn
50 | const msg = abiCoder.encode(['address', 'uint', 'uint', 'tuple(address, uint, bytes)[]'], [ambireAccountAddress, 31337, 0, txns])
51 | const publicKeys = [pkPairOne.publicKey, pkPairTwo.publicKey]
52 | const publicNonces = [schnorrkel.generatePublicNonces(pkPairOne.privateKey), schnorrkel.generatePublicNonces(pkPairTwo.privateKey)]
53 | const hashFn = ethers.utils.keccak256
54 | const {signature: sigOne} = schnorrkel.multiSigSign(pkPairOne.privateKey, msg, publicKeys, publicNonces, hashFn)
55 | const {signature: sigTwo, challenge, finalPublicNonce} = schnorrkel.multiSigSign(pkPairTwo.privateKey, msg, publicKeys, publicNonces, hashFn)
56 | const schnorrSignature = Schnorrkel.sumSigs([sigOne, sigTwo])
57 | const verification = Schnorrkel.verify(schnorrSignature, msg, finalPublicNonce, combinedPublicKey, hashFn)
58 | expect(verification).to.equal(true)
59 |
60 | // // wrap the schnorr signature and validate that it is valid
61 | const sigData = abiCoder.encode([ 'bytes32', 'bytes32', 'bytes32', 'uint8' ], [
62 | px,
63 | challenge.buffer,
64 | schnorrSignature.buffer,
65 | parity
66 | ])
67 | const ambireSig = wrapSchnorr(sigData)
68 | const alabala = await factory.deployAndExecute(bytecode, 0, txns, ambireSig)
69 | console.log(alabala)
70 | }
71 |
72 | deployAndExecute()
--------------------------------------------------------------------------------
/scripts/erc4337/examplePimlico.js:
--------------------------------------------------------------------------------
1 | const { SimpleAccountFactory__factory, EntryPoint__factory, SimpleAccount__factory } = require("@account-abstraction/contracts")
2 | const { StaticJsonRpcProvider } = require("@ethersproject/providers")
3 | const { Wallet } = require("ethers")
4 | const { arrayify, hexlify, getAddress, hexConcat } = require("ethers/lib/utils")
5 | const { rpcs, chains } = require("./config")
6 | require('dotenv').config();
7 |
8 | const run = async () => {
9 | // GENERATE THE INITCODE
10 | const SIMPLE_ACCOUNT_FACTORY_ADDRESS = "0x9406Cc6185a346906296840746125a0E44976454"
11 | const provider = new StaticJsonRpcProvider(rpcs.mumbai)
12 | const owner = Wallet.createRandom()
13 |
14 | const simpleAccountFactory = SimpleAccountFactory__factory.connect(SIMPLE_ACCOUNT_FACTORY_ADDRESS, provider)
15 | const initCode = hexConcat([
16 | SIMPLE_ACCOUNT_FACTORY_ADDRESS,
17 | simpleAccountFactory.interface.encodeFunctionData("createAccount", [owner.address, 0])
18 | ])
19 |
20 | // CALCULATE THE SENDER ADDRESS
21 | const ENTRY_POINT_ADDRESS = "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789"
22 |
23 | const entryPoint = EntryPoint__factory.connect(ENTRY_POINT_ADDRESS, provider)
24 |
25 | const senderAddress = await entryPoint.callStatic
26 | .getSenderAddress(initCode)
27 | .then(() => {
28 | throw new Error("Expected getSenderAddress() to revert")
29 | })
30 | .catch((e) => {
31 | const data = e.message.match(/0x6ca7b806([a-fA-F\d]*)/)?.[1]
32 | if (!data) {
33 | return Promise.reject(new Error("Failed to parse revert data"))
34 | }
35 | const addr = getAddress(`0x${data.slice(24, 64)}`)
36 | return Promise.resolve(addr)
37 | })
38 |
39 | // GENERATE THE CALLDATA
40 | const to = "0xCB8B547f2895475838195ee52310BD2422544408" // test metamask addr
41 | const value = 0
42 | const data = "0x68656c6c6f" // "hello" encoded to to utf-8 bytes
43 |
44 | const simpleAccount = SimpleAccount__factory.connect(senderAddress, provider)
45 | const callData = simpleAccount.interface.encodeFunctionData("execute", [to, value, data])
46 |
47 | // FILL OUT THE REMAINING USEROPERATION VALUES
48 | const gasPrice = await provider.getGasPrice()
49 |
50 | const userOperation = {
51 | sender: senderAddress,
52 | nonce: hexlify(0),
53 | initCode,
54 | callData,
55 | callGasLimit: hexlify(100_000), // hardcode it for now at a high value
56 | verificationGasLimit: hexlify(400_000), // hardcode it for now at a high value
57 | preVerificationGas: hexlify(50_000), // hardcode it for now at a high value
58 | maxFeePerGas: hexlify(gasPrice),
59 | maxPriorityFeePerGas: hexlify(gasPrice),
60 | paymasterAndData: "0x",
61 | signature: "0x"
62 | }
63 |
64 | // REQUEST PIMLICO VERIFYING PAYMASTER SPONSORSHIP
65 | const apiKey = process.env.PIMLICO_API_KEY
66 |
67 | const pimlicoEndpoint = `https://api.pimlico.io/v1/${chains.mumbai}/rpc?apikey=${apiKey}`
68 |
69 | const pimlicoProvider = new StaticJsonRpcProvider(pimlicoEndpoint)
70 |
71 | const sponsorUserOperationResult = await pimlicoProvider.send("pm_sponsorUserOperation", [
72 | userOperation,
73 | {
74 | entryPoint: ENTRY_POINT_ADDRESS
75 | }
76 | ])
77 |
78 | const paymasterAndData = sponsorUserOperationResult.paymasterAndData
79 |
80 | userOperation.paymasterAndData = paymasterAndData
81 |
82 | // SIGN THE USEROPERATION
83 | const signature = await owner.signMessage(arrayify(await entryPoint.getUserOpHash(userOperation)))
84 |
85 | userOperation.signature = signature
86 |
87 | // SUBMIT THE USEROPERATION TO BE BUNDLED
88 | const userOperationHash = await pimlicoProvider.send("eth_sendUserOperation", [userOperation, ENTRY_POINT_ADDRESS])
89 | console.log("UserOperation hash:", userOperationHash)
90 |
91 | // let's also wait for the userOperation to be included, by continually querying for the receipts
92 | console.log("Querying for receipts...")
93 | let receipt = null
94 | let counter = 0
95 | while (receipt === null) {
96 | try {
97 | await new Promise((r) => setTimeout(r, 1000)) //sleep
98 | counter++
99 | receipt = await pimlicoProvider.send("eth_getUserOperationReceipt", [userOperationHash])
100 | console.log(receipt)
101 | } catch (e) {
102 | console.log('error throwed, retry counter ' + counter)
103 | }
104 | }
105 |
106 | const txHash = receipt.receipt.transactionHash
107 | console.log(`${txHash}`)
108 | }
109 |
110 | run()
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Created by https://www.toptal.com/developers/gitignore/api/linux,macos,react,visualstudiocode,node
2 | # Edit at https://www.toptal.com/developers/gitignore?templates=linux,macos,react,visualstudiocode,node
3 |
4 | ### Linux ###
5 | *~
6 |
7 | # temporary files which can be created if a process still has a handle open of a deleted file
8 | .fuse_hidden*
9 |
10 | # KDE directory preferences
11 | .directory
12 |
13 | # Linux trash folder which might appear on any partition or disk
14 | .Trash-*
15 |
16 | # .nfs files are created when an open file is removed but is still being accessed
17 | .nfs*
18 |
19 | ### macOS ###
20 | # General
21 | .DS_Store
22 | .AppleDouble
23 | .LSOverride
24 |
25 | # Icon must end with two \r
26 | Icon
27 |
28 |
29 | # Thumbnails
30 | ._*
31 |
32 | # Files that might appear in the root of a volume
33 | .DocumentRevisions-V100
34 | .fseventsd
35 | .Spotlight-V100
36 | .TemporaryItems
37 | .Trashes
38 | .VolumeIcon.icns
39 | .com.apple.timemachine.donotpresent
40 |
41 | # Directories potentially created on remote AFP share
42 | .AppleDB
43 | .AppleDesktop
44 | Network Trash Folder
45 | Temporary Items
46 | .apdisk
47 |
48 | ### macOS Patch ###
49 | # iCloud generated files
50 | *.icloud
51 |
52 | ### Node ###
53 | # Logs
54 | logs
55 | *.log
56 | npm-debug.log*
57 | yarn-debug.log*
58 | yarn-error.log*
59 |
60 | /build
61 | /artifacts
62 | /cache
63 | /typechain-types
64 |
65 | lerna-debug.log*
66 | .pnpm-debug.log*
67 |
68 | # Diagnostic reports (https://nodejs.org/api/report.html)
69 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
70 |
71 | # Runtime data
72 | pids
73 | *.pid
74 | *.seed
75 | *.pid.lock
76 |
77 | # Directory for instrumented libs generated by jscoverage/JSCover
78 | lib-cov
79 |
80 | # Coverage directory used by tools like istanbul
81 | coverage
82 | *.lcov
83 |
84 | # nyc test coverage
85 | .nyc_output
86 |
87 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
88 | .grunt
89 |
90 | # Bower dependency directory (https://bower.io/)
91 | bower_components
92 |
93 | # node-waf configuration
94 | .lock-wscript
95 |
96 | # Compiled binary addons (https://nodejs.org/api/addons.html)
97 | build/Release
98 |
99 | # Dependency directories
100 | node_modules/
101 | jspm_packages/
102 |
103 | # Snowpack dependency directory (https://snowpack.dev/)
104 | web_modules/
105 |
106 | # TypeScript cache
107 | *.tsbuildinfo
108 |
109 | # Optional npm cache directory
110 | .npm
111 |
112 | # Optional eslint cache
113 | .eslintcache
114 |
115 | # Optional stylelint cache
116 | .stylelintcache
117 |
118 | # Microbundle cache
119 | .rpt2_cache/
120 | .rts2_cache_cjs/
121 | .rts2_cache_es/
122 | .rts2_cache_umd/
123 |
124 | # Optional REPL history
125 | .node_repl_history
126 |
127 | # Output of 'npm pack'
128 | *.tgz
129 |
130 | # Yarn Integrity file
131 | .yarn-integrity
132 |
133 | # dotenv environment variable files
134 | .env
135 | .env.development.local
136 | .env.test.local
137 | .env.production.local
138 | .env.local
139 |
140 | # parcel-bundler cache (https://parceljs.org/)
141 | .cache
142 | .parcel-cache
143 |
144 | # Next.js build output
145 | .next
146 | out
147 |
148 | # Nuxt.js build / generate output
149 | .nuxt
150 | dist
151 |
152 | # Gatsby files
153 | .cache/
154 | # Comment in the public line in if your project uses Gatsby and not Next.js
155 | # https://nextjs.org/blog/next-9-1#public-directory-support
156 | # public
157 |
158 | # vuepress build output
159 | .vuepress/dist
160 |
161 | # vuepress v2.x temp and cache directory
162 | .temp
163 |
164 | # Docusaurus cache and generated files
165 | .docusaurus
166 |
167 | # Serverless directories
168 | .serverless/
169 |
170 | # FuseBox cache
171 | .fusebox/
172 |
173 | # DynamoDB Local files
174 | .dynamodb/
175 |
176 | # TernJS port file
177 | .tern-port
178 |
179 | # Stores VSCode versions used for testing VSCode extensions
180 | .vscode-test
181 |
182 | # yarn v2
183 | .yarn/cache
184 | .yarn/unplugged
185 | .yarn/build-state.yml
186 | .yarn/install-state.gz
187 | .pnp.*
188 |
189 | ### Node Patch ###
190 | # Serverless Webpack directories
191 | .webpack/
192 |
193 | # Optional stylelint cache
194 |
195 | # SvelteKit build / generate output
196 | .svelte-kit
197 |
198 | ### react ###
199 | .DS_*
200 | **/*.backup.*
201 | **/*.back.*
202 |
203 | node_modules
204 |
205 | *.sublime*
206 |
207 | psd
208 | thumb
209 | sketch
210 |
211 | ### VisualStudioCode ###
212 | .vscode/
213 | !.vscode/settings.json
214 | !.vscode/tasks.json
215 | !.vscode/launch.json
216 | !.vscode/extensions.json
217 | !.vscode/*.code-snippets
218 |
219 | # Local History for Visual Studio Code
220 | .history/
221 |
222 | # Built Visual Studio Code Extensions
223 | *.vsix
224 |
225 | ### VisualStudioCode Patch ###
226 | # Ignore all local history of files
227 | .history
228 | .ionide
229 |
230 | # End of https://www.toptal.com/developers/gitignore/api/linux,macos,react,visualstudiocode,node
231 |
--------------------------------------------------------------------------------
/test/createSender.test.js:
--------------------------------------------------------------------------------
1 | const { ethers } = require('hardhat')
2 | const ERC4337Account = require('../artifacts/contracts/ERC4337Account.sol/ERC4337Account.json')
3 | const { expect } = require('chai')
4 | const { chainIds } = require('../scripts/erc4337/config')
5 | const { default: Schnorrkel, Key } = require('@borislav.itskov/schnorrkel.js')
6 |
7 | const salt = '0x0'
8 | function getAddressCreateTwo(factoryAddress, bytecode) {
9 | return ethers.utils.getCreate2Address(factoryAddress, ethers.utils.hexZeroPad(salt, 32), ethers.utils.keccak256(bytecode))
10 | }
11 |
12 | const localhost = 'http://127.0.0.1:8545'
13 |
14 | function getDeployCalldata(bytecodeWithArgs, salt2) {
15 | const setAddrPrivilegeABI = ['function deploy(bytes calldata code, uint256 salt) external']
16 | const iface = new ethers.utils.Interface(setAddrPrivilegeABI)
17 | return iface.encodeFunctionData('deploy', [
18 | bytecodeWithArgs,
19 | salt2
20 | ])
21 | }
22 |
23 | function getExecuteCalldata(txns, signature) {
24 | const abi = ['function execute(tuple(address, uint, bytes)[] calldata txns, bytes calldata signature) public payable']
25 | const iface = new ethers.utils.Interface(abi)
26 | return iface.encodeFunctionData('execute', [
27 | txns,
28 | signature
29 | ])
30 | }
31 |
32 | function wrapSchnorr(sig) {
33 | return `${sig}${'04'}`
34 | }
35 |
36 | function getSchnorrAddress(pk) {
37 | const publicKey = ethers.utils.arrayify(ethers.utils.computePublicKey(ethers.utils.arrayify(pk), true))
38 | const px = ethers.utils.hexlify(publicKey.slice(1, 33))
39 | const hash = ethers.utils.keccak256(ethers.utils.solidityPack(['string', 'bytes'], ['SCHNORR', px]))
40 | return '0x' + hash.slice(hash.length - 40, hash.length)
41 | }
42 |
43 |
44 | describe('create sender tests', function(){
45 | it('should work with ambire factory and account', async function(){
46 | const pk = '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80'
47 | const [signer, otherSigner] = await ethers.getSigners()
48 | const factory = await ethers.deployContract('AmbireAccountFactory', [signer.address])
49 | const ENTRY_POINT_ADDRESS = "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789"
50 |
51 | const abicoder = new ethers.utils.AbiCoder()
52 | const schnorrVirtualAddr = getSchnorrAddress(pk)
53 | const bytecodeWithArgs = ethers.utils.concat([
54 | ERC4337Account.bytecode,
55 | abicoder.encode(['address', 'address[]'], [ENTRY_POINT_ADDRESS, [signer.address, schnorrVirtualAddr]])
56 | ])
57 |
58 | const calldata = getDeployCalldata(bytecodeWithArgs, salt)
59 | const initcode = ethers.utils.hexlify(ethers.utils.concat([factory.address, calldata]))
60 |
61 | const senderCreator = await ethers.deployContract('SenderCreator')
62 | await senderCreator.createSender(initcode)
63 |
64 | const senderAddress = getAddressCreateTwo(factory.address, bytecodeWithArgs)
65 | const acc = new ethers.Contract(senderAddress, ERC4337Account.abi, signer)
66 | const entryPointAddr = await acc.entryPoint()
67 | expect(entryPointAddr).to.equal(ENTRY_POINT_ADDRESS)
68 |
69 | // const entryPoint = await ethers.deployContract('EntryPoint')
70 | // const createAcc = await entryPoint.createSenderIfNeeded(senderAddress, ethers.utils.hexlify(2_000_000), initcode)
71 |
72 | // GENERATE THE CALLDATA
73 | const to = "0xCB8B547f2895475838195ee52310BD2422544408" // test metamask addr
74 | const value = 0
75 | const data = "0x68656c6c6f" // "hello" encoded to to utf-8 bytes
76 |
77 | // VERIFY SCHNORR IS WORKING ON CHAIN
78 | const singleTxn = [to, value, data]
79 | const txns = [singleTxn]
80 | const msg = abicoder.encode(['address', 'uint', 'uint', 'tuple(address, uint, bytes)[]'], [senderAddress, 31337, 0, txns])
81 | const hashFn = ethers.utils.keccak256
82 | const schnorrPrivateKey = new Key(Buffer.from(ethers.utils.arrayify(pk)))
83 | const schnorrSig = Schnorrkel.sign(schnorrPrivateKey, msg, hashFn)
84 | const publicKey = ethers.utils.arrayify(ethers.utils.computePublicKey(ethers.utils.arrayify(pk), true))
85 | const schnorrPublicKey = new Key(Buffer.from(ethers.utils.arrayify(publicKey)))
86 | const verification = Schnorrkel.verify(
87 | schnorrSig.signature,
88 | msg,
89 | schnorrSig.finalPublicNonce,
90 | schnorrPublicKey,
91 | hashFn
92 | )
93 | console.log(verification)
94 |
95 | // wrap the schnorr signature and validate that it is valid
96 | const px = ethers.utils.hexlify(schnorrPublicKey.buffer.slice(1, 33))
97 | const parity = schnorrPublicKey.buffer[0] - 2 + 27
98 | const sigData = abicoder.encode([ 'bytes32', 'bytes32', 'bytes32', 'uint8' ], [
99 | px,
100 | schnorrSig.challenge.buffer,
101 | schnorrSig.signature.buffer,
102 | parity
103 | ])
104 | const ambireSig = wrapSchnorr(sigData)
105 | const executeCalldata = getExecuteCalldata(txns, ambireSig)
106 |
107 | const txn = await signer.sendTransaction({
108 | to: senderAddress,
109 | data: executeCalldata
110 | })
111 | const receipt = await txn.wait()
112 | console.log(receipt)
113 | })
114 | })
--------------------------------------------------------------------------------
/contracts/libs/SignatureValidator.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: agpl-3.0
2 | pragma solidity 0.8.19;
3 |
4 | import './Bytes.sol';
5 |
6 | interface IERC1271Wallet {
7 | function isValidSignature(bytes32 hash, bytes calldata signature) external view returns (bytes4 magicValue);
8 | }
9 |
10 | library SignatureValidator {
11 | using Bytes for bytes;
12 |
13 | enum SignatureMode {
14 | EIP712,
15 | EthSign,
16 | SmartWallet,
17 | Spoof,
18 | Schnorr,
19 | Multisig,
20 | // WARNING: must always be last
21 | LastUnused
22 | }
23 |
24 | // bytes4(keccak256("isValidSignature(bytes32,bytes)"))
25 | bytes4 internal constant ERC1271_MAGICVALUE_BYTES32 = 0x1626ba7e;
26 | // secp256k1 group order
27 | uint256 internal constant Q = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141;
28 |
29 | function splitSignature(bytes memory sig) internal pure returns (bytes memory, uint8) {
30 | uint8 modeRaw;
31 | unchecked {
32 | modeRaw = uint8(sig[sig.length - 1]);
33 | }
34 | sig.trimToSize(sig.length - 1);
35 | return (sig, modeRaw);
36 | }
37 |
38 | function recoverAddr(bytes32 hash, bytes memory sig) internal view returns (address) {
39 | return recoverAddrImpl(hash, sig, false);
40 | }
41 |
42 | function recoverAddrImpl(bytes32 hash, bytes memory sig, bool allowSpoofing) internal view returns (address) {
43 | require(sig.length != 0, 'SV_SIGLEN');
44 | uint8 modeRaw;
45 | unchecked {
46 | modeRaw = uint8(sig[sig.length - 1]);
47 | }
48 | // Ensure we're in bounds for mode; Solidity does this as well but it will just silently blow up rather than showing a decent error
49 | require(modeRaw < uint8(SignatureMode.LastUnused), 'SV_SIGMODE');
50 | SignatureMode mode = SignatureMode(modeRaw);
51 |
52 | // {r}{s}{v}{mode}
53 | if (mode == SignatureMode.EIP712 || mode == SignatureMode.EthSign) {
54 | require(sig.length == 66, 'SV_LEN');
55 | bytes32 r = sig.readBytes32(0);
56 | bytes32 s = sig.readBytes32(32);
57 | uint8 v = uint8(sig[64]);
58 | if (mode == SignatureMode.EthSign) hash = keccak256(abi.encodePacked('\x19Ethereum Signed Message:\n32', hash));
59 | address signer = ecrecover(hash, v, r, s);
60 | require(signer != address(0), 'SV_ZERO_SIG');
61 | return signer;
62 | // {sig}{verifier}{mode}
63 | } else if (mode == SignatureMode.Schnorr) {
64 | // Based on https://hackmd.io/@nZ-twauPRISEa6G9zg3XRw/SyjJzSLt9
65 | // You can use this library to produce signatures: https://github.com/borislav-itskov/schnorrkel.js
66 | // px := public key x-coord
67 | // e := schnorr signature challenge
68 | // s := schnorr signature
69 | // parity := public key y-coord parity (27 or 28)
70 | // last uint8 is for the Ambire sig mode - it's ignored
71 | sig.trimToSize(sig.length - 1);
72 | (bytes32 px, bytes32 e, bytes32 s, uint8 parity) = abi.decode(sig, (bytes32, bytes32, bytes32, uint8));
73 | // ecrecover = (m, v, r, s);
74 | bytes32 sp = bytes32(Q - mulmod(uint256(s), uint256(px), Q));
75 | bytes32 ep = bytes32(Q - mulmod(uint256(e), uint256(px), Q));
76 |
77 | require(sp != bytes32(Q));
78 | // the ecrecover precompile implementation checks that the `r` and `s`
79 | // inputs are non-zero (in this case, `px` and `ep`), thus we don't need to
80 | // check if they're zero.
81 | address R = ecrecover(sp, parity, px, ep);
82 | require(R != address(0), 'SV_ZERO_SIG');
83 | require(e == keccak256(abi.encodePacked(R, uint8(parity), px, hash)), 'SV_SCHNORR_FAILED');
84 | return address(uint160(uint256(keccak256(abi.encodePacked('SCHNORR', px)))));
85 | } else if (mode == SignatureMode.Multisig) {
86 | sig.trimToSize(sig.length - 1);
87 | bytes[] memory signatures = abi.decode(sig, (bytes[]));
88 | address signer;
89 | for (uint256 i = 0; i != signatures.length; i++) {
90 | signer = address(
91 | uint160(uint256(keccak256(abi.encodePacked(signer, recoverAddrImpl(hash, signatures[i], false)))))
92 | );
93 | }
94 | require(signer != address(0), 'SV_ZERO_SIG');
95 | return signer;
96 | } else if (mode == SignatureMode.SmartWallet) {
97 | // 32 bytes for the addr, 1 byte for the type = 33
98 | require(sig.length > 33, 'SV_LEN_WALLET');
99 | uint256 newLen;
100 | unchecked {
101 | newLen = sig.length - 33;
102 | }
103 | IERC1271Wallet wallet = IERC1271Wallet(address(uint160(uint256(sig.readBytes32(newLen)))));
104 | sig.trimToSize(newLen);
105 | require(ERC1271_MAGICVALUE_BYTES32 == wallet.isValidSignature(hash, sig), 'SV_WALLET_INVALID');
106 | address signer = address(wallet);
107 | require(signer != address(0), 'SV_ZERO_SIG');
108 | return signer;
109 | // {address}{mode}; the spoof mode is used when simulating calls
110 | } else if (mode == SignatureMode.Spoof && allowSpoofing) {
111 | // This is safe cause it's specifically intended for spoofing sigs in simulation conditions, where tx.origin can be controlled
112 | // We did not choose 0x00..00 because in future network upgrades tx.origin may be nerfed or there may be edge cases in which
113 | // it is zero, such as native account abstraction
114 | // slither-disable-next-line tx-origin
115 | require(tx.origin == address(1) || tx.origin == address(6969), 'SV_SPOOF_ORIGIN');
116 | require(sig.length == 33, 'SV_SPOOF_LEN');
117 | sig.trimToSize(32);
118 | // To simulate the gas usage; check is just to silence unused warning
119 | require(ecrecover(0, 0, 0, 0) != address(6969));
120 | return abi.decode(sig, (address));
121 | }
122 | revert('SV_TYPE');
123 | }
124 | }
125 |
--------------------------------------------------------------------------------
/src/config/service-worker-registration.js:
--------------------------------------------------------------------------------
1 | // This optional code is used to register a service worker.
2 | // register() is not called by default.
3 |
4 | // This lets the app load faster on subsequent visits in production, and gives
5 | // it offline capabilities. However, it also means that developers (and users)
6 | // will only see deployed updates on subsequent visits to a page, after all the
7 | // existing tabs open on the page have been closed, since previously cached
8 | // resources are updated in the background.
9 |
10 | // To learn more about the benefits of this model and instructions on how to
11 | // opt-in, read https://cra.link/PWA
12 |
13 | const isLocalhost = Boolean(
14 | window.location.hostname === "localhost" ||
15 | // [::1] is the IPv6 localhost address.
16 | window.location.hostname === "[::1]" ||
17 | // 127.0.0.0/8 are considered localhost for IPv4.
18 | window.location.hostname.match(
19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
20 | )
21 | );
22 |
23 | export function register(config) {
24 | if (process.env.NODE_ENV === "production" && "serviceWorker" in navigator) {
25 | // The URL constructor is available in all browsers that support SW.
26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
27 | if (publicUrl.origin !== window.location.origin) {
28 | // Our service worker won't work if PUBLIC_URL is on a different origin
29 | // from what our page is served on. This might happen if a CDN is used to
30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374
31 | return;
32 | }
33 |
34 | window.addEventListener("load", () => {
35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
36 |
37 | if (isLocalhost) {
38 | // This is running on localhost. Let's check if a service worker still exists or not.
39 | checkValidServiceWorker(swUrl, config);
40 |
41 | // Add some additional logging to localhost, pointing developers to the
42 | // service worker/PWA documentation.
43 | navigator.serviceWorker.ready.then(() => {
44 | console.log(
45 | "This web app is being served cache-first by a service " +
46 | "worker. To learn more, visit https://cra.link/PWA"
47 | );
48 | });
49 | } else {
50 | // Is not localhost. Just register service worker
51 | registerValidSW(swUrl, config);
52 | }
53 | });
54 | }
55 | }
56 |
57 | function registerValidSW(swUrl, config) {
58 | navigator.serviceWorker
59 | .register(swUrl)
60 | .then((registration) => {
61 | registration.onupdatefound = () => {
62 | const installingWorker = registration.installing;
63 | if (installingWorker == null) {
64 | return;
65 | }
66 | installingWorker.onstatechange = () => {
67 | if (installingWorker.state === "installed") {
68 | if (navigator.serviceWorker.controller) {
69 | // At this point, the updated precached content has been fetched,
70 | // but the previous service worker will still serve the older
71 | // content until all client tabs are closed.
72 | console.log(
73 | "New content is available and will be used when all " +
74 | "tabs for this page are closed. See https://cra.link/PWA."
75 | );
76 |
77 | // Execute callback
78 | if (config && config.onUpdate) {
79 | config.onUpdate(registration);
80 | }
81 | } else {
82 | // At this point, everything has been precached.
83 | // It's the perfect time to display a
84 | // "Content is cached for offline use." message.
85 | console.log("Content is cached for offline use.");
86 |
87 | // Execute callback
88 | if (config && config.onSuccess) {
89 | config.onSuccess(registration);
90 | }
91 | }
92 | }
93 | };
94 | };
95 | })
96 | .catch((error) => {
97 | console.error("Error during service worker registration:", error);
98 | });
99 | }
100 |
101 | function checkValidServiceWorker(swUrl, config) {
102 | // Check if the service worker can be found. If it can't reload the page.
103 | fetch(swUrl, {
104 | headers: { "Service-Worker": "script" },
105 | })
106 | .then((response) => {
107 | // Ensure service worker exists, and that we really are getting a JS file.
108 | const contentType = response.headers.get("content-type");
109 | if (
110 | response.status === 404 ||
111 | (contentType != null && contentType.indexOf("javascript") === -1)
112 | ) {
113 | // No service worker found. Probably a different app. Reload the page.
114 | navigator.serviceWorker.ready.then((registration) => {
115 | registration.unregister().then(() => {
116 | window.location.reload();
117 | });
118 | });
119 | } else {
120 | // Service worker found. Proceed as normal.
121 | registerValidSW(swUrl, config);
122 | }
123 | })
124 | .catch(() => {
125 | console.log(
126 | "No internet connection found. App is running in offline mode."
127 | );
128 | });
129 | }
130 |
131 | export function unregister() {
132 | if ("serviceWorker" in navigator) {
133 | navigator.serviceWorker.ready
134 | .then((registration) => {
135 | registration.unregister();
136 | })
137 | .catch((error) => {
138 | console.error(error.message);
139 | });
140 | }
141 | }
142 |
--------------------------------------------------------------------------------
/src/App.tsx:
--------------------------------------------------------------------------------
1 | import logo from "./qr-seal-logo-transparent.png";
2 | import {
3 | Flex,
4 | Box,
5 | Heading,
6 | Step,
7 | StepIndicator,
8 | StepSeparator,
9 | StepStatus,
10 | StepTitle,
11 | Stepper,
12 | Text,
13 | Link,
14 | useColorModeValue,
15 | Stack,
16 | Container
17 | } from "@chakra-ui/react";
18 |
19 | import Accounts from "./common/accounts";
20 | import JoinMultisig from "./multisig/components/JoinMultisig";
21 | import CreateTransaction from "./multisig/components/CreateTransaction";
22 | import CoSign from "./multisig/components/CoSign";
23 | import { useEOA } from "./auth/context/eoa";
24 | import { useSteps } from "./auth/context/step";
25 |
26 | const steps = [
27 | { title: "EOA" },
28 | { title: "Multisig" },
29 | { title: "Transaction" },
30 | ];
31 |
32 | function App() {
33 | const { eoaAddress } = useEOA()
34 | const { activeStep } = useSteps()
35 | const isMobile = window.innerWidth <= 430
36 |
37 | return (
38 |
45 |
50 |
51 |
59 |
66 |
67 | QR Seal
68 |
69 |
76 | Privacy-Preserving, Gas-Optimized Multisig
77 |
via Account Abstraction, ERC-4337 & Schnorr 🤿 Signatures.
78 |
79 |
80 |
81 |
89 | {steps.map((step, index) => (
90 |
91 | activeStep ? "teal.500" : "teal.300"}>
92 |
97 |
98 |
99 | {/* @ts-ignore */}
100 |
101 | {step.title}
102 |
103 |
104 | {/* @ts-ignore */}
105 | activeStep ? "teal.500" : "teal.300"} />
106 |
107 | ))}
108 |
109 |
110 | {/* TODO: Figure out on a later step if we should leave this installation button */}
111 | {/* */}
112 |
113 |
114 |
115 | {eoaAddress && }
116 |
117 |
118 |
119 | {activeStep === 2 && }
120 | {activeStep >= 1 && }
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
131 |
135 |
143 | Build at the
144 | ETHPrague hackathon 2023 🇨🇿
145 | | Made with ❤️ by team GoodMorning |
146 | Open Source
147 |
148 |
149 |
150 |
151 |
152 |
153 | );
154 | }
155 |
156 | export default App;
157 |
--------------------------------------------------------------------------------
/scripts/erc4337/ambireAccountViaPimlico.js:
--------------------------------------------------------------------------------
1 | const { EntryPoint__factory } = require("@account-abstraction/contracts")
2 | const { StaticJsonRpcProvider } = require("@ethersproject/providers")
3 | const { ethers } = require("ethers")
4 | const { rpcs, chainIds, chains, factoryAddr } = require("./config")
5 | require('dotenv').config();
6 | const { default: Schnorrkel, Key } = require('@borislav.itskov/schnorrkel.js')
7 | const ERC4337Account = require('../../artifacts/contracts/ERC4337Account.sol/ERC4337Account.json')
8 | const AmbireAccount = require('../../artifacts/contracts/AmbireAccount.sol/AmbireAccount.json')
9 | const salt = '0x0'
10 |
11 | function wrapSchnorr(sig) {
12 | return `${sig}${'04'}`
13 | }
14 |
15 | function getAmbireAccountAddress(factoryAddress, bytecode) {
16 | return ethers.utils.getCreate2Address(factoryAddress, ethers.utils.hexZeroPad(salt, 32), ethers.utils.keccak256(bytecode))
17 | }
18 |
19 | function getSchnorrAddress(pk) {
20 | const publicKey = ethers.utils.arrayify(ethers.utils.computePublicKey(ethers.utils.arrayify(pk), true))
21 | const px = ethers.utils.hexlify(publicKey.slice(1, 33))
22 | const hash = ethers.utils.keccak256(ethers.utils.solidityPack(['string', 'bytes'], ['SCHNORR', px]))
23 | return '0x' + hash.slice(hash.length - 40, hash.length)
24 | }
25 |
26 | function getDeployCalldata(bytecodeWithArgs, salt2) {
27 | const abi = ['function deploy(bytes calldata code, uint256 salt) external']
28 | const iface = new ethers.utils.Interface(abi)
29 | return iface.encodeFunctionData('deploy', [
30 | bytecodeWithArgs,
31 | salt2
32 | ])
33 | }
34 |
35 | function getExecuteCalldata(txns) {
36 | const abi = ['function executeBySender(tuple(address, uint, bytes)[] calldata txns) external payable']
37 | const iface = new ethers.utils.Interface(abi)
38 | return iface.encodeFunctionData('executeBySender', [txns])
39 | }
40 |
41 | const run = async () => {
42 | const someWallet = ethers.Wallet.createRandom()
43 | const pk = someWallet.privateKey
44 | const AMBIRE_ACCOUNT_FACTORY_ADDR = factoryAddr.mumbai
45 | const ENTRY_POINT_ADDRESS = "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789"
46 | const provider = new StaticJsonRpcProvider(rpcs.mumbai)
47 | const entryPoint = EntryPoint__factory.connect(ENTRY_POINT_ADDRESS, provider)
48 | const abicoder = new ethers.utils.AbiCoder()
49 | const schnorrVirtualAddr = getSchnorrAddress(pk)
50 | const bytecodeWithArgs = ethers.utils.concat([
51 | ERC4337Account.bytecode,
52 | abicoder.encode(['address', 'address[]'], [ENTRY_POINT_ADDRESS, [schnorrVirtualAddr]])
53 | ])
54 |
55 | const senderAddress = getAmbireAccountAddress(AMBIRE_ACCOUNT_FACTORY_ADDR, bytecodeWithArgs)
56 | const code = await provider.getCode(senderAddress)
57 | const hasCode = code !== '0x'
58 | const initCode = hasCode
59 | ? '0x'
60 | : ethers.utils.hexlify(ethers.utils.concat([
61 | AMBIRE_ACCOUNT_FACTORY_ADDR,
62 | getDeployCalldata(bytecodeWithArgs, salt)
63 | ]))
64 | const entryPointNonce = await entryPoint.getNonce(senderAddress, 0)
65 | const userOpNonce = entryPointNonce.toHexString()
66 |
67 | // GENERATE THE CALLDATA
68 | const to = "0xCB8B547f2895475838195ee52310BD2422544408" // test metamask addr
69 | const value = 0
70 | const data = "0x68656c6c6f" // "hello" encoded to to utf-8 bytes
71 | const singleTxn = [to, value, data]
72 | const txns = [singleTxn]
73 | const executeCalldata = getExecuteCalldata(txns)
74 |
75 | // // FILL OUT THE REMAINING USEROPERATION VALUES
76 | const gasPrice = await provider.getGasPrice()
77 |
78 | const userOperation = {
79 | sender: senderAddress,
80 | nonce: userOpNonce,
81 | initCode,
82 | callData: executeCalldata,
83 | callGasLimit: ethers.utils.hexlify(100_000), // hardcode it for now at a high value
84 | verificationGasLimit: ethers.utils.hexlify(2_000_000), // hardcode it for now at a high value
85 | preVerificationGas: ethers.utils.hexlify(50_000), // hardcode it for now at a high value
86 | maxFeePerGas: ethers.utils.hexlify(gasPrice),
87 | maxPriorityFeePerGas: ethers.utils.hexlify(gasPrice),
88 | paymasterAndData: "0x",
89 | signature: "0x"
90 | }
91 |
92 | // REQUEST PIMLICO VERIFYING PAYMASTER SPONSORSHIP
93 | const apiKey = process.env.PIMLICO_API_KEY
94 | const pimlicoEndpoint = `https://api.pimlico.io/v1/${chains.mumbai}/rpc?apikey=${apiKey}`
95 | const pimlicoProvider = new StaticJsonRpcProvider(pimlicoEndpoint)
96 | const sponsorUserOperationResult = await pimlicoProvider.send("pm_sponsorUserOperation", [
97 | userOperation,
98 | {
99 | entryPoint: ENTRY_POINT_ADDRESS
100 | }
101 | ])
102 | const paymasterAndData = sponsorUserOperationResult.paymasterAndData
103 | userOperation.paymasterAndData = paymasterAndData
104 |
105 | // SIGN THE USEROPERATION
106 | const schnorrPrivateKey = new Key(Buffer.from(ethers.utils.arrayify(pk)))
107 | const userOpHash = await entryPoint.getUserOpHash(userOperation)
108 | const signature = Schnorrkel.signHash(schnorrPrivateKey, userOpHash)
109 | const publicKey = ethers.utils.arrayify(ethers.utils.computePublicKey(ethers.utils.arrayify(pk), true))
110 | const schnorrPublicKey = new Key(Buffer.from(ethers.utils.arrayify(publicKey)))
111 | const verificationUserOp = Schnorrkel.verifyHash(
112 | signature.signature,
113 | userOpHash,
114 | signature.finalPublicNonce,
115 | schnorrPublicKey
116 | )
117 | console.log(verificationUserOp)
118 | const px = ethers.utils.hexlify(schnorrPublicKey.buffer.slice(1, 33))
119 | const parity = schnorrPublicKey.buffer[0] - 2 + 27
120 | const sigDataUserOp = abicoder.encode([ 'bytes32', 'bytes32', 'bytes32', 'uint8' ], [
121 | px,
122 | signature.challenge.buffer,
123 | signature.signature.buffer,
124 | parity
125 | ])
126 | const wrappedSig = wrapSchnorr(sigDataUserOp)
127 | userOperation.signature = wrappedSig
128 |
129 | // const abi = ['function simulateHandleOp(tuple(address, uint256, bytes, bytes, uint256, uint256, uint256, uint256, uint256, bytes, bytes) calldata op, address target, bytes calldata targetCallData)']
130 | // const iface = new ethers.utils.Interface(abi)
131 | // const simulateData = iface.encodeFunctionData('simulateHandleOp', [
132 | // Object.values(userOperation),
133 | // senderAddress,
134 | // executeCalldata
135 | // ])
136 | // const validation = await provider.call({
137 | // to: ENTRY_POINT_ADDRESS,
138 | // data: simulateData
139 | // })
140 | // const decodeAbi = ['error ExecutionResult(uint256, uint256, uint48, uint48, bool, bytes)']
141 | // const iface2 = new ethers.utils.Interface(decodeAbi)
142 | // const selector = validation.slice(0, 10);
143 | // const decoded = iface2.decodeErrorResult(selector, validation)
144 | // console.log(decoded)
145 |
146 | // const ambireWallet = new ethers.Contract(senderAddress, AmbireAccount.abi)
147 | // const iface3 = ambireWallet.interface;
148 | // const selector2 = decoded[5].slice(0, 10);
149 | // const error = iface3.decodeErrorResult(selector2, decoded[5])
150 | // console.log(error)
151 |
152 | // SUBMIT THE USEROPERATION TO BE BUNDLED
153 | const userOperationHash = await pimlicoProvider.send("eth_sendUserOperation", [userOperation, ENTRY_POINT_ADDRESS])
154 | console.log("UserOperation hash:", userOperationHash)
155 |
156 | // let's also wait for the userOperation to be included, by continually querying for the receipts
157 | console.log("Querying for receipts...")
158 | let receipt = null
159 | let counter = 0
160 | while (receipt === null) {
161 | try {
162 | await new Promise((r) => setTimeout(r, 1000)) //sleep
163 | counter++
164 | receipt = await pimlicoProvider.send("eth_getUserOperationReceipt", [userOperationHash])
165 | console.log(receipt)
166 | } catch (e) {
167 | console.log('error throwed, retry counter ' + counter)
168 | }
169 | }
170 |
171 | const txHash = receipt.receipt.transactionHash
172 | console.log(`${txHash}`)
173 | }
174 |
175 | run()
--------------------------------------------------------------------------------
/src/multisig/components/CreateTransaction.tsx:
--------------------------------------------------------------------------------
1 | import Schnorrkel, { Key } from "@borislav.itskov/schnorrkel.js";
2 | import { ethers } from "ethers";
3 | import { useContext, useState } from "react";
4 | import QRCode from "react-qr-code";
5 | import { useForm } from "react-hook-form";
6 | import {
7 | Modal,
8 | ModalContent,
9 | ModalOverlay,
10 | useDisclosure,
11 | FormLabel,
12 | FormControl,
13 | Input,
14 | Button,
15 | } from "@chakra-ui/react";
16 | import MultisigContext from "../../auth/context/multisig";
17 | import { getSchnorrkelInstance } from "../../singletons/Schnorr";
18 | import { useEOA } from "../../auth/context/eoa";
19 | import { ENTRY_POINT_ADDRESS, FACTORY_ADDRESS, mainProvider } from "../../config/constants";
20 | import { EntryPoint__factory } from "@account-abstraction/contracts"
21 | import ERC4337Account from '../../builds/ERC4337Account.json'
22 | import { computeSchnorrAddress, getDeployCalldata, getExecuteCalldata } from "../../utils/helpers";
23 |
24 | interface FormProps {
25 | to: string;
26 | value: number;
27 | }
28 |
29 | const CreateTransaction = (props: any) => {
30 | const { eoaPrivateKey, eoaPublicKey } = useEOA()
31 | const { getAllMultisigData } = useContext(MultisigContext)
32 |
33 | const { isOpen, onOpen, onClose } = useDisclosure();
34 | const {
35 | isOpen: isQrOpen,
36 | onOpen: onQrOpen,
37 | onClose: onQrClose,
38 | } = useDisclosure();
39 | const [qrCodeValue, setQrCodeValue] = useState("");
40 | const {
41 | handleSubmit,
42 | register,
43 | formState: { isSubmitting },
44 | } = useForm();
45 |
46 | const onSubmit = async (values: FormProps) => {
47 | const data = getAllMultisigData();
48 | if (!data) return
49 |
50 | const abiCoder = new ethers.utils.AbiCoder();
51 | const sendTosignerTxn = [
52 | values.to,
53 | ethers.utils.parseEther(values.value.toString()),
54 | "0x00",
55 | ];
56 | const txns = [sendTosignerTxn];
57 | // TO DO: the nonce is hardcoded to 0 here.
58 | // change it to read from the contract if any
59 | const publicKeyOne = new Key(
60 | Buffer.from(ethers.utils.arrayify(eoaPublicKey))
61 | );
62 | const publicKeyTwo = new Key(
63 | Buffer.from(ethers.utils.arrayify(data.multisigPartnerPublicKey))
64 | );
65 | const publicKeys = [publicKeyOne, publicKeyTwo];
66 | const schnorrkel = getSchnorrkelInstance()
67 | const privateKey = new Key(
68 | Buffer.from(ethers.utils.arrayify(eoaPrivateKey))
69 | );
70 | const partnerNonces = {
71 | kPublic: Key.fromHex(data.multisigPartnerKPublicHex),
72 | kTwoPublic: Key.fromHex(data.multisigPartnerKTwoPublicHex),
73 | };
74 |
75 | const publicNonces = schnorrkel.generatePublicNonces(privateKey);
76 | const combinedPublicNonces = [publicNonces, partnerNonces];
77 |
78 | // configure the user operation
79 | const combinedPublicKey = Schnorrkel.getCombinedPublicKey(publicKeys);
80 | const schnorrVirtualAddr = computeSchnorrAddress(combinedPublicKey)
81 | const entryPoint = EntryPoint__factory.connect(ENTRY_POINT_ADDRESS, mainProvider)
82 | const entryPointNonce = await entryPoint.getNonce(data.multisigAddr, 0)
83 | const userOpNonce = entryPointNonce.toHexString()
84 | const bytecodeWithArgs = ethers.utils.concat([
85 | ERC4337Account.bytecode,
86 | abiCoder.encode(['address', 'address[]'], [ENTRY_POINT_ADDRESS, [schnorrVirtualAddr]])
87 | ])
88 | const initCode = ethers.utils.hexlify(ethers.utils.concat([
89 | FACTORY_ADDRESS,
90 | getDeployCalldata(bytecodeWithArgs)
91 | ]))
92 | const executeCalldata = getExecuteCalldata([txns])
93 | const gasPrice = await mainProvider.getGasPrice()
94 | const hexGasPrice = ethers.utils.hexlify(gasPrice)
95 | const userOperation = {
96 | sender: data.multisigAddr,
97 | nonce: userOpNonce,
98 | initCode,
99 | callData: executeCalldata,
100 | callGasLimit: ethers.utils.hexlify(100_000), // hardcode it for now at a high value
101 | verificationGasLimit: ethers.utils.hexlify(2_000_000), // hardcode it for now at a high value
102 | preVerificationGas: ethers.utils.hexlify(50_000), // hardcode it for now at a high value
103 | maxFeePerGas: hexGasPrice,
104 | maxPriorityFeePerGas: hexGasPrice,
105 | paymasterAndData: "0x",
106 | signature: "0x"
107 | }
108 |
109 | // REQUEST PIMLICO VERIFYING PAYMASTER SPONSORSHIP
110 | const apiKey = process.env.REACT_APP_PIMLICO_API_KEY
111 | const pimlicoEndpoint = `https://api.pimlico.io/v1/polygon/rpc?apikey=${apiKey}`
112 | const pimlicoProvider = new ethers.providers.StaticJsonRpcProvider(pimlicoEndpoint)
113 | const sponsorUserOperationResult = await pimlicoProvider.send("pm_sponsorUserOperation", [
114 | userOperation,
115 | {
116 | entryPoint: ENTRY_POINT_ADDRESS
117 | }
118 | ])
119 | const paymasterAndData = sponsorUserOperationResult.paymasterAndData
120 | userOperation.paymasterAndData = paymasterAndData
121 |
122 | const userOpHash = await entryPoint.getUserOpHash(userOperation)
123 | const { signature } = schnorrkel.multiSigSignHash(
124 | privateKey,
125 | userOpHash,
126 | publicKeys,
127 | combinedPublicNonces
128 | );
129 | const sigHex = signature.toHex()
130 | const kPublicHex = publicNonces.kPublic.toHex();
131 | const kTwoPublicHex = publicNonces.kTwoPublic.toHex();
132 | const qrCode =
133 | eoaPublicKey +
134 | "|" +
135 | kPublicHex +
136 | "|" +
137 | kTwoPublicHex +
138 | "|" +
139 | sigHex +
140 | "|" +
141 | values.to +
142 | "|" +
143 | values.value +
144 | "|" +
145 | hexGasPrice +
146 | "|" +
147 | paymasterAndData
148 | setQrCodeValue(qrCode);
149 | onQrOpen();
150 | return new Promise((resolve) => resolve(true));
151 | };
152 |
153 | return (
154 | <>
155 |
156 |
157 |
158 |
159 | Create Transaction
160 |
196 |
197 |
198 | {/* the qr code modal */}
199 |
200 |
201 |
202 |
203 |
204 |
205 | >
206 | );
207 | };
208 |
209 | export default CreateTransaction;
210 |
--------------------------------------------------------------------------------
/test/DeployFactory.test.js:
--------------------------------------------------------------------------------
1 | const { ethers } = require('ethers')
2 | const { expect } = require('chai')
3 | const { getProxyDeployBytecode, getStorageSlotsFromArtifact } = require('../src/deploy/getBytecodeJs')
4 | const { buildInfo, AmbireAccount, AmbireAccountFactory } = require('./config')
5 | const { default: Schnorrkel } = require('@borislav.itskov/schnorrkel.js')
6 | const { generateRandomKeys } = require('@borislav.itskov/schnorrkel.js/dist/core/index.js')
7 | const schnorrkel = new Schnorrkel()
8 |
9 | const salt = '0x0'
10 | function getAmbireAccountAddress(factoryAddress, bytecode) {
11 | return ethers.utils.getCreate2Address(factoryAddress, ethers.utils.hexZeroPad(salt, 32), ethers.utils.keccak256(bytecode))
12 | }
13 | function wrapSchnorr(sig) {
14 | return `${sig}${'04'}`
15 | }
16 |
17 | const FACTORY_ADDRESS = "0x1bb0684486c35e35D56FaA806e12f6819dbe9527";
18 | const AMBIRE_ADDRESS = "0x1100E4Cf3fe64b928cccE36c78ad6b7696d72446";
19 | const localhost = 'http://127.0.0.1:8545'
20 | const mainProvider = new ethers.providers.JsonRpcProvider(localhost)
21 |
22 | describe('AmbireAccountFactory tests', function(){
23 | it('should deploy the factory, deploy the contract, execute a multisignature schnorr transaction', async function(){
24 | const [signer] = await ethers.getSigners()
25 | const factory = await ethers.deployContract('AmbireAccountFactory', [signer.address])
26 |
27 | // configure the schnorr virtual address
28 | const pkPairOne = generateRandomKeys()
29 | const pkPairTwo = generateRandomKeys()
30 | const combinedPublicKey = Schnorrkel.getCombinedPublicKey([
31 | pkPairOne.publicKey,
32 | pkPairTwo.publicKey
33 | ])
34 | const px = ethers.utils.hexlify(combinedPublicKey.buffer.slice(1, 33))
35 | const parity = combinedPublicKey.buffer[0] - 2 + 27
36 | const hash = ethers.utils.keccak256(ethers.utils.solidityPack(['string', 'bytes'], ['SCHNORR', px]))
37 | const combinedPublicAddress = '0x' + hash.slice(hash.length - 40, hash.length)
38 |
39 | // deploy the ambire account
40 | const contract = await ethers.deployContract('AmbireAccount')
41 | const bytecode = getProxyDeployBytecode(contract.address, [{addr: combinedPublicAddress, hash: true}], {
42 | ...getStorageSlotsFromArtifact(buildInfo)
43 | })
44 | await factory.deploy(bytecode, 0)
45 | const ambireAccountAddress = getAmbireAccountAddress(factory.address, bytecode)
46 |
47 | // give money to the multisig address
48 | await signer.sendTransaction({
49 | to: ambireAccountAddress,
50 | value: ethers.utils.parseEther('200'),
51 | })
52 |
53 | // send money to the signer txn
54 | const abiCoder = new ethers.utils.AbiCoder()
55 | const sendTosignerTxn = [signer.address, ethers.utils.parseEther('2'), '0x00']
56 | const txns = [sendTosignerTxn]
57 | const msg = abiCoder.encode(['address', 'uint', 'uint', 'tuple(address, uint, bytes)[]'], [ambireAccountAddress, 31337, 0, txns])
58 | const publicKeys = [pkPairOne.publicKey, pkPairTwo.publicKey]
59 | const publicNonces = [schnorrkel.generatePublicNonces(pkPairOne.privateKey), schnorrkel.generatePublicNonces(pkPairTwo.privateKey)]
60 | const hashFn = ethers.utils.keccak256
61 | const {signature: sigOne, challenge, finalPublicNonce} = schnorrkel.multiSigSign(pkPairOne.privateKey, msg, publicKeys, publicNonces, hashFn)
62 | const {signature: sigTwo} = schnorrkel.multiSigSign(pkPairTwo.privateKey, msg, publicKeys, publicNonces, hashFn)
63 | const schnorrSignature = Schnorrkel.sumSigs([sigOne, sigTwo])
64 | const verification = Schnorrkel.verify(schnorrSignature, msg, finalPublicNonce, combinedPublicKey, hashFn)
65 | expect(verification).to.equal(true)
66 |
67 | const ambireAccount = new ethers.Contract(ambireAccountAddress, AmbireAccount.abi, signer)
68 | const canSign = await ambireAccount.privileges(combinedPublicAddress)
69 | expect(canSign).to.equal('0x0000000000000000000000000000000000000000000000000000000000000001')
70 |
71 | // wrap the schnorr signature and validate that it is valid
72 | const sigData = abiCoder.encode([ 'bytes32', 'bytes32', 'bytes32', 'uint8' ], [
73 | px,
74 | challenge.buffer,
75 | schnorrSignature.buffer,
76 | parity
77 | ])
78 | const ambireSig = wrapSchnorr(sigData)
79 | await ambireAccount.execute(txns, ambireSig)
80 |
81 | const balance = ethers.utils.formatEther(await signer.getBalance())
82 | expect(balance > 9801).to.equal(true)
83 | expect(balance < 9802).to.equal(true)
84 | })
85 | it('should deploy the factory and call deployAndExecute and deploy the ambire account executing a multisignature schnorr transaction', async function(){
86 | const [signer, otherSigner] = await ethers.getSigners()
87 | const factory = await ethers.deployContract('AmbireAccountFactory', [otherSigner.address])
88 |
89 | // configure the schnorr virtual address
90 | const pkPairOne = generateRandomKeys()
91 | const pkPairTwo = generateRandomKeys()
92 | const combinedPublicKey = Schnorrkel.getCombinedPublicKey([
93 | pkPairOne.publicKey,
94 | pkPairTwo.publicKey
95 | ])
96 | const px = ethers.utils.hexlify(combinedPublicKey.buffer.slice(1, 33))
97 | const parity = combinedPublicKey.buffer[0] - 2 + 27
98 | const hash = ethers.utils.keccak256(ethers.utils.solidityPack(['string', 'bytes'], ['SCHNORR', px]))
99 | const combinedPublicAddress = '0x' + hash.slice(hash.length - 40, hash.length)
100 |
101 | // deploy the ambire account
102 | const contract = await ethers.deployContract('AmbireAccount')
103 | const bytecode = getProxyDeployBytecode(contract.address, [{addr: combinedPublicAddress, hash: true}], {
104 | ...getStorageSlotsFromArtifact(buildInfo)
105 | })
106 | const abiCoder = new ethers.utils.AbiCoder()
107 | const sendTosignerTxn = [otherSigner.address, ethers.utils.parseEther('2'), '0x00']
108 | const txns = [sendTosignerTxn]
109 |
110 | const ambireAccountAddress = getAmbireAccountAddress(factory.address, bytecode)
111 |
112 | // give money to the multisig address
113 | await otherSigner.sendTransaction({
114 | to: ambireAccountAddress,
115 | value: ethers.utils.parseEther('200'),
116 | })
117 |
118 | // // send money to the signer txn
119 | const msg = abiCoder.encode(['address', 'uint', 'uint', 'tuple(address, uint, bytes)[]'], [ambireAccountAddress, 31337, 0, txns])
120 | const publicKeys = [pkPairOne.publicKey, pkPairTwo.publicKey]
121 | const publicNonces = [schnorrkel.generatePublicNonces(pkPairOne.privateKey), schnorrkel.generatePublicNonces(pkPairTwo.privateKey)]
122 | const hashFn = ethers.utils.keccak256
123 | const {signature: sigOne, challenge, finalPublicNonce} = schnorrkel.multiSigSign(pkPairOne.privateKey, msg, publicKeys, publicNonces, hashFn)
124 | const {signature: sigTwo} = schnorrkel.multiSigSign(pkPairTwo.privateKey, msg, publicKeys, publicNonces, hashFn)
125 | const schnorrSignature = Schnorrkel.sumSigs([sigOne, sigTwo])
126 | const verification = Schnorrkel.verify(schnorrSignature, msg, finalPublicNonce, combinedPublicKey, hashFn)
127 | expect(verification).to.equal(true)
128 |
129 | // // wrap the schnorr signature and validate that it is valid
130 | const sigData = abiCoder.encode([ 'bytes32', 'bytes32', 'bytes32', 'uint8' ], [
131 | px,
132 | challenge.buffer,
133 | schnorrSignature.buffer,
134 | parity
135 | ])
136 | const ambireSig = wrapSchnorr(sigData)
137 | await factory.deployAndExecute(bytecode, 0, txns, ambireSig)
138 |
139 | const balance = ethers.utils.formatEther(await otherSigner.getBalance())
140 | expect(balance > 9801).to.equal(true)
141 | expect(balance < 9802).to.equal(true)
142 | })
143 |
144 | it('change to localhost', async function(){
145 | const balanceAtTheBeg = await mainProvider.getBalance('0xCD4D4a1955852c6dC2b8fd7E3FEB7724373DB9Cc')
146 | console.log(balanceAtTheBeg)
147 |
148 | const pkWithETH = '0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d'
149 | const otherSigner = new ethers.Wallet(pkWithETH, mainProvider)
150 | const factory = new ethers.Contract(FACTORY_ADDRESS, AmbireAccountFactory.abi, otherSigner)
151 |
152 | // configure the schnorr virtual address
153 | const pkPairOne = generateRandomKeys()
154 | const pkPairTwo = generateRandomKeys()
155 | const combinedPublicKey = Schnorrkel.getCombinedPublicKey([
156 | pkPairOne.publicKey,
157 | pkPairTwo.publicKey
158 | ])
159 | const px = ethers.utils.hexlify(combinedPublicKey.buffer.slice(1, 33))
160 | const parity = combinedPublicKey.buffer[0] - 2 + 27
161 | const hash = ethers.utils.keccak256(ethers.utils.solidityPack(['string', 'bytes'], ['SCHNORR', px]))
162 | const combinedPublicAddress = '0x' + hash.slice(hash.length - 40, hash.length)
163 |
164 | // deploy the ambire account
165 | const bytecode = getProxyDeployBytecode(AMBIRE_ADDRESS, [{addr: combinedPublicAddress, hash: true}], {
166 | ...getStorageSlotsFromArtifact(buildInfo)
167 | })
168 | const abiCoder = new ethers.utils.AbiCoder()
169 | const sendTosignerTxn = ['0xCD4D4a1955852c6dC2b8fd7E3FEB7724373DB9Cc', ethers.utils.parseEther('2'), '0x00']
170 | const txns = [sendTosignerTxn]
171 |
172 | const ambireAccountAddress = getAmbireAccountAddress(factory.address, bytecode)
173 |
174 | // give money to the multisig address
175 | await otherSigner.sendTransaction({
176 | to: ambireAccountAddress,
177 | value: ethers.utils.parseEther('200'),
178 | })
179 |
180 | // send money to the signer txn
181 | const msg = abiCoder.encode(['address', 'uint', 'uint', 'tuple(address, uint, bytes)[]'], [ambireAccountAddress, 31337, 0, txns])
182 | const publicKeys = [pkPairOne.publicKey, pkPairTwo.publicKey]
183 | const publicNonces = [schnorrkel.generatePublicNonces(pkPairOne.privateKey), schnorrkel.generatePublicNonces(pkPairTwo.privateKey)]
184 | const hashFn = ethers.utils.keccak256
185 | const {signature: sigOne, challenge, finalPublicNonce} = schnorrkel.multiSigSign(pkPairOne.privateKey, msg, publicKeys, publicNonces, hashFn)
186 | const {signature: sigTwo} = schnorrkel.multiSigSign(pkPairTwo.privateKey, msg, publicKeys, publicNonces, hashFn)
187 | const schnorrSignature = Schnorrkel.sumSigs([sigOne, sigTwo])
188 | const verification = Schnorrkel.verify(schnorrSignature, msg, finalPublicNonce, combinedPublicKey, hashFn)
189 | expect(verification).to.equal(true)
190 |
191 | // // wrap the schnorr signature and validate that it is valid
192 | const sigData = abiCoder.encode([ 'bytes32', 'bytes32', 'bytes32', 'uint8' ], [
193 | px,
194 | challenge.buffer,
195 | schnorrSignature.buffer,
196 | parity
197 | ])
198 | const ambireSig = wrapSchnorr(sigData)
199 | const alabala = await factory.deployAndExecute(bytecode, 0, txns, ambireSig)
200 | await alabala.wait()
201 |
202 | const balanceAtTheEnd = await mainProvider.getBalance('0xCD4D4a1955852c6dC2b8fd7E3FEB7724373DB9Cc')
203 | console.log(balanceAtTheEnd)
204 | })
205 | })
--------------------------------------------------------------------------------
/src/builds/AmbireAccountFactory.json:
--------------------------------------------------------------------------------
1 | {
2 | "_format": "hh-sol-artifact-1",
3 | "contractName": "AmbireAccountFactory",
4 | "sourceName": "contracts/AmbireAccountFactory.sol",
5 | "abi": [
6 | {
7 | "inputs": [
8 | {
9 | "internalType": "address",
10 | "name": "allowed",
11 | "type": "address"
12 | }
13 | ],
14 | "stateMutability": "nonpayable",
15 | "type": "constructor"
16 | },
17 | {
18 | "anonymous": false,
19 | "inputs": [
20 | {
21 | "indexed": false,
22 | "internalType": "address",
23 | "name": "addr",
24 | "type": "address"
25 | },
26 | {
27 | "indexed": false,
28 | "internalType": "uint256",
29 | "name": "salt",
30 | "type": "uint256"
31 | }
32 | ],
33 | "name": "LogDeployed",
34 | "type": "event"
35 | },
36 | {
37 | "inputs": [],
38 | "name": "allowedToDrain",
39 | "outputs": [
40 | {
41 | "internalType": "address",
42 | "name": "",
43 | "type": "address"
44 | }
45 | ],
46 | "stateMutability": "view",
47 | "type": "function"
48 | },
49 | {
50 | "inputs": [
51 | {
52 | "internalType": "address",
53 | "name": "to",
54 | "type": "address"
55 | },
56 | {
57 | "internalType": "uint256",
58 | "name": "value",
59 | "type": "uint256"
60 | },
61 | {
62 | "internalType": "bytes",
63 | "name": "data",
64 | "type": "bytes"
65 | },
66 | {
67 | "internalType": "uint256",
68 | "name": "gas",
69 | "type": "uint256"
70 | }
71 | ],
72 | "name": "call",
73 | "outputs": [],
74 | "stateMutability": "nonpayable",
75 | "type": "function"
76 | },
77 | {
78 | "inputs": [
79 | {
80 | "internalType": "bytes",
81 | "name": "code",
82 | "type": "bytes"
83 | },
84 | {
85 | "internalType": "uint256",
86 | "name": "salt",
87 | "type": "uint256"
88 | }
89 | ],
90 | "name": "deploy",
91 | "outputs": [],
92 | "stateMutability": "nonpayable",
93 | "type": "function"
94 | },
95 | {
96 | "inputs": [
97 | {
98 | "internalType": "bytes",
99 | "name": "code",
100 | "type": "bytes"
101 | },
102 | {
103 | "internalType": "uint256",
104 | "name": "salt",
105 | "type": "uint256"
106 | },
107 | {
108 | "components": [
109 | {
110 | "internalType": "address",
111 | "name": "to",
112 | "type": "address"
113 | },
114 | {
115 | "internalType": "uint256",
116 | "name": "value",
117 | "type": "uint256"
118 | },
119 | {
120 | "internalType": "bytes",
121 | "name": "data",
122 | "type": "bytes"
123 | }
124 | ],
125 | "internalType": "struct AmbireAccount.Transaction[]",
126 | "name": "txns",
127 | "type": "tuple[]"
128 | },
129 | {
130 | "internalType": "bytes",
131 | "name": "signature",
132 | "type": "bytes"
133 | }
134 | ],
135 | "name": "deployAndExecute",
136 | "outputs": [],
137 | "stateMutability": "nonpayable",
138 | "type": "function"
139 | }
140 | ],
141 | "bytecode": "0x60a03461007157601f61082b38819003918201601f19168301916001600160401b038311848410176100765780849260209460405283398101031261007157516001600160a01b03811681036100715760805260405161079e908161008d823960805181818161011401526101ea0152f35b600080fd5b634e487b7160e01b600052604160045260246000fdfe6080604052600436101561001257600080fd5b6000803560e01c9081631ce6236f146100fa5781631e86da2a14610055575080634e4a0c821461005057639c4ae2d01461004b57600080fd5b6102ad565b6101a5565b346100f75760803660031901126100f75767ffffffffffffffff6004358181116100f35761008790369060040161014d565b604492919235908282116100ef57366023830112156100ef578160040135908382116100eb573660248360051b850101116100eb576064359384116100eb576100e8946100da602495369060040161014d565b9590940191602435916103fe565b80f35b8580fd5b8480fd5b8280fd5b80fd5b346100f757806003193601126100f7576001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001660805260206080f35b600091031261014857565b600080fd5b9181601f840112156101485782359167ffffffffffffffff8311610148576020838186019501011161014857565b600435906001600160a01b038216820361014857565b35906001600160a01b038216820361014857565b34610148576080366003190112610148576101be61017b565b60443567ffffffffffffffff8111610148576101de90369060040161014d565b91906001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000163303610269576100e89183604051948593843782019060009485938385809552039160243590606435f13d15610261573d906102458261037e565b91610253604051938461035c565b82523d84602084013e610567565b606090610567565b606460405162461bcd60e51b815260206004820152600f60248201527f4f4e4c595f415554484f52495a454400000000000000000000000000000000006044820152fd5b346101485760403660031901126101485760043567ffffffffffffffff8111610148576102f16102e46102f692369060040161014d565b919060243592369161039a565b610664565b005b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b67ffffffffffffffff811161033b57604052565b6102f8565b6080810190811067ffffffffffffffff82111761033b57604052565b90601f8019910116810190811067ffffffffffffffff82111761033b57604052565b67ffffffffffffffff811161033b57601f01601f191660200190565b9291926103a68261037e565b916103b4604051938461035c565b829481845281830111610148578281602093846000960137010152565b908060209392818452848401376000828201840152601f01601f1916010190565b6040513d6000823e3d90fd5b959293949161041c906102f16001600160a01b03988994369161039a565b1693843b15610148579492919060409283519687957f6171d1c900000000000000000000000000000000000000000000000000000000875281604488018760048a015252606487019260648360051b8901019681946000925b8584106104cb5750505050505050836104a060009694829488946003198584030160248601526103d1565b03925af180156104c6576104b15750565b806104be6104c492610327565b8061013d565b565b6103f2565b919395995091939596976063198c82030183528935605e1983360301811215610148578201866104fa82610191565b168252602090818101358284015285810135601e1982360301811215610148570181813591019267ffffffffffffffff821161014857813603841361014857600193839261055192606090818b82015201916103d1565b9b019301940191949290938b9998979694610475565b1561056f5750565b6040519062461bcd60e51b82528160208060048301528251908160248401526000935b8285106105b5575050604492506000838284010152601f80199101168101030190fd5b8481018201518686016044015293810193859350610592565b156105d557565b606460405162461bcd60e51b815260206004820152601060248201527f4641494c45445f4445504c4f59494e47000000000000000000000000000000006044820152fd5b1561062057565b606460405162461bcd60e51b815260206004820152600c60248201527f4641494c45445f4d4154434800000000000000000000000000000000000000006044820152fd5b908151906106eb6106df6020850193842060405160208101917fff0000000000000000000000000000000000000000000000000000000000000083526bffffffffffffffffffffffff193060601b1660218301528560358301526055820152605581526106d081610340565b5190206001600160a01b031690565b6001600160a01b031690565b92833b156106fa575b50505090565b7fecef66cbb4d4c8dd18157def75d46290ddc298395ea46f7ff64321c1a912cbad92829151906000f56107456001600160a01b038083169061073d8215156105ce565b861614610619565b604080516001600160a01b039290921682526020820192909252a13880806106f456fea26469706673582212200500d7745ccf67273f48b26043eb23404c43b5a54379192174665ed9ca485a8864736f6c63430008130033",
142 | "deployedBytecode": "0x6080604052600436101561001257600080fd5b6000803560e01c9081631ce6236f146100fa5781631e86da2a14610055575080634e4a0c821461005057639c4ae2d01461004b57600080fd5b6102ad565b6101a5565b346100f75760803660031901126100f75767ffffffffffffffff6004358181116100f35761008790369060040161014d565b604492919235908282116100ef57366023830112156100ef578160040135908382116100eb573660248360051b850101116100eb576064359384116100eb576100e8946100da602495369060040161014d565b9590940191602435916103fe565b80f35b8580fd5b8480fd5b8280fd5b80fd5b346100f757806003193601126100f7576001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001660805260206080f35b600091031261014857565b600080fd5b9181601f840112156101485782359167ffffffffffffffff8311610148576020838186019501011161014857565b600435906001600160a01b038216820361014857565b35906001600160a01b038216820361014857565b34610148576080366003190112610148576101be61017b565b60443567ffffffffffffffff8111610148576101de90369060040161014d565b91906001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000163303610269576100e89183604051948593843782019060009485938385809552039160243590606435f13d15610261573d906102458261037e565b91610253604051938461035c565b82523d84602084013e610567565b606090610567565b606460405162461bcd60e51b815260206004820152600f60248201527f4f4e4c595f415554484f52495a454400000000000000000000000000000000006044820152fd5b346101485760403660031901126101485760043567ffffffffffffffff8111610148576102f16102e46102f692369060040161014d565b919060243592369161039a565b610664565b005b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b67ffffffffffffffff811161033b57604052565b6102f8565b6080810190811067ffffffffffffffff82111761033b57604052565b90601f8019910116810190811067ffffffffffffffff82111761033b57604052565b67ffffffffffffffff811161033b57601f01601f191660200190565b9291926103a68261037e565b916103b4604051938461035c565b829481845281830111610148578281602093846000960137010152565b908060209392818452848401376000828201840152601f01601f1916010190565b6040513d6000823e3d90fd5b959293949161041c906102f16001600160a01b03988994369161039a565b1693843b15610148579492919060409283519687957f6171d1c900000000000000000000000000000000000000000000000000000000875281604488018760048a015252606487019260648360051b8901019681946000925b8584106104cb5750505050505050836104a060009694829488946003198584030160248601526103d1565b03925af180156104c6576104b15750565b806104be6104c492610327565b8061013d565b565b6103f2565b919395995091939596976063198c82030183528935605e1983360301811215610148578201866104fa82610191565b168252602090818101358284015285810135601e1982360301811215610148570181813591019267ffffffffffffffff821161014857813603841361014857600193839261055192606090818b82015201916103d1565b9b019301940191949290938b9998979694610475565b1561056f5750565b6040519062461bcd60e51b82528160208060048301528251908160248401526000935b8285106105b5575050604492506000838284010152601f80199101168101030190fd5b8481018201518686016044015293810193859350610592565b156105d557565b606460405162461bcd60e51b815260206004820152601060248201527f4641494c45445f4445504c4f59494e47000000000000000000000000000000006044820152fd5b1561062057565b606460405162461bcd60e51b815260206004820152600c60248201527f4641494c45445f4d4154434800000000000000000000000000000000000000006044820152fd5b908151906106eb6106df6020850193842060405160208101917fff0000000000000000000000000000000000000000000000000000000000000083526bffffffffffffffffffffffff193060601b1660218301528560358301526055820152605581526106d081610340565b5190206001600160a01b031690565b6001600160a01b031690565b92833b156106fa575b50505090565b7fecef66cbb4d4c8dd18157def75d46290ddc298395ea46f7ff64321c1a912cbad92829151906000f56107456001600160a01b038083169061073d8215156105ce565b861614610619565b604080516001600160a01b039290921682526020820192909252a13880806106f456fea26469706673582212200500d7745ccf67273f48b26043eb23404c43b5a54379192174665ed9ca485a8864736f6c63430008130033",
143 | "linkReferences": {},
144 | "deployedLinkReferences": {}
145 | }
146 |
--------------------------------------------------------------------------------
/contracts/AmbireAccount.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: agpl-3.0
2 | pragma solidity 0.8.19;
3 |
4 | import './libs/SignatureValidator.sol';
5 |
6 | // @dev All external/public functions (that are not view/pure) use `payable` because AmbireAccount
7 | // is a wallet contract, and any ETH sent to it is not lost, but on the other hand not having `payable`
8 | // makes the Solidity compiler add an extra check for `msg.value`, which in this case is wasted gas
9 | contract AmbireAccount {
10 | // @dev We do not have a constructor. This contract cannot be initialized with any valid `privileges` by itself!
11 | // The indended use case is to deploy one base implementation contract, and create a minimal proxy for each user wallet, by
12 | // using our own code generation to insert SSTOREs to initialize `privileges` (IdentityProxyDeploy.js)
13 | address private constant FALLBACK_HANDLER_SLOT = address(0x6969);
14 |
15 | // Variables
16 | mapping(address => bytes32) public privileges;
17 | uint256 public nonce;
18 | mapping(bytes32 => uint) public scheduledRecoveries;
19 |
20 | // Events
21 | event LogPrivilegeChanged(address indexed addr, bytes32 priv);
22 | event LogErr(address indexed to, uint256 value, bytes data, bytes returnData); // only used in tryCatch
23 | event LogRecoveryScheduled(
24 | bytes32 indexed txnHash,
25 | bytes32 indexed recoveryHash,
26 | address indexed recoveryKey,
27 | uint256 nonce,
28 | uint256 time,
29 | Transaction[] txns
30 | );
31 | event LogRecoveryCancelled(
32 | bytes32 indexed txnHash,
33 | bytes32 indexed recoveryHash,
34 | address indexed recoveryKey,
35 | uint256 time
36 | );
37 | event LogRecoveryFinalized(bytes32 indexed txnHash, bytes32 indexed recoveryHash, uint256 time);
38 |
39 | // Transaction structure
40 | // we handle replay protection separately by requiring (address(this), chainID, nonce) as part of the sig
41 | // @dev a better name for this would be `Call`, but we are keeping `Transaction` for backwards compatibility
42 | struct Transaction {
43 | address to;
44 | uint256 value;
45 | bytes data;
46 | }
47 | struct RecoveryInfo {
48 | address[] keys;
49 | uint256 timelock;
50 | }
51 | // built-in batching of multiple execute()'s; useful when performing timelocked recoveries
52 | struct ExecuteArgs {
53 | Transaction[] txns;
54 | bytes signature;
55 | }
56 |
57 | // Recovery mode constants
58 | uint8 private constant SIGMODE_CANCEL = 254;
59 | uint8 private constant SIGMODE_RECOVER = 255;
60 |
61 | // This contract can accept ETH without calldata
62 | receive() external payable {}
63 |
64 | // To support EIP 721 and EIP 1155, we need to respond to those methods with their own method signature
65 | function onERC721Received(address, address, uint256, bytes calldata) external pure returns (bytes4) {
66 | return this.onERC721Received.selector;
67 | }
68 |
69 | function onERC1155Received(address, address, uint256, uint256, bytes calldata) external pure returns (bytes4) {
70 | return this.onERC1155Received.selector;
71 | }
72 |
73 | function onERC1155BatchReceived(
74 | address,
75 | address,
76 | uint256[] calldata,
77 | uint256[] calldata,
78 | bytes calldata
79 | ) external pure returns (bytes4) {
80 | return this.onERC1155BatchReceived.selector;
81 | }
82 |
83 | // @notice fallback method: currently used to call the fallback handler
84 | // which is set by the user and can be changed
85 | // @dev this contract can accept ETH with calldata, hence payable
86 | fallback() external payable {
87 | // We store the fallback handler at this magic slot
88 | address fallbackHandler = address(uint160(uint(privileges[FALLBACK_HANDLER_SLOT])));
89 | if (fallbackHandler == address(0)) return;
90 | assembly {
91 | // We can use memory addr 0, since it's not occupied
92 | calldatacopy(0, 0, calldatasize())
93 | let result := delegatecall(gas(), fallbackHandler, 0, calldatasize(), 0, 0)
94 | let size := returndatasize()
95 | returndatacopy(0, 0, size)
96 | if eq(result, 0) {
97 | revert(0, size)
98 | }
99 | return(0, size)
100 | }
101 | }
102 |
103 | // @notice used to set the privilege of a key (by `addr`); normal signatures will be considered valid if the
104 | // `addr` they are signed with has non-zero (not 0x000..000) privilege set; we can set the privilege to
105 | // a hash of the recovery keys and timelock (see `RecoveryInfo`) to enable recovery signatures
106 | function setAddrPrivilege(address addr, bytes32 priv) external payable {
107 | require(msg.sender == address(this), 'ONLY_IDENTITY_CAN_CALL');
108 | privileges[addr] = priv;
109 | emit LogPrivilegeChanged(addr, priv);
110 | }
111 |
112 | // @notice Useful when we need to do multiple operations but ignore failures in some of them
113 | function tryCatch(address to, uint256 value, bytes calldata data) external payable {
114 | require(msg.sender == address(this), 'ONLY_IDENTITY_CAN_CALL');
115 | (bool success, bytes memory returnData) = to.call{ value: value, gas: gasleft() }(data);
116 | if (!success) emit LogErr(to, value, data, returnData);
117 | }
118 |
119 | // @notice same as `tryCatch` but with a gas limit
120 | function tryCatchLimit(address to, uint256 value, bytes calldata data, uint256 gasLimit) external payable {
121 | require(msg.sender == address(this), 'ONLY_IDENTITY_CAN_CALL');
122 | (bool success, bytes memory returnData) = to.call{ value: value, gas: gasLimit }(data);
123 | if (!success) emit LogErr(to, value, data, returnData);
124 | }
125 |
126 | // @notice execute: this method is used to execute a single bundle of calls that are signed with a key
127 | // that is authorized to execute on this account (in `privileges`)
128 | // @dev: WARNING: if the signature of this is changed, we have to change AmbireAccountFactory
129 | function execute(Transaction[] calldata txns, bytes calldata signature) public payable {
130 |
131 | uint256 currentNonce = nonce;
132 | // NOTE: abi.encode is safer than abi.encodePacked in terms of collision safety
133 | bytes32 hash = keccak256(abi.encode(address(this), block.chainid, currentNonce, txns));
134 |
135 | address signerKey;
136 | // Recovery signature: allows to perform timelocked txns
137 | uint8 sigMode = uint8(signature[signature.length - 1]);
138 |
139 | if (sigMode == SIGMODE_RECOVER || sigMode == SIGMODE_CANCEL) {
140 | (bytes memory sig, ) = SignatureValidator.splitSignature(signature);
141 | (RecoveryInfo memory recoveryInfo, bytes memory innerRecoverySig, address signerKeyToRecover) = abi.decode(
142 | sig,
143 | (RecoveryInfo, bytes, address)
144 | );
145 | signerKey = signerKeyToRecover;
146 | bool isCancellation = sigMode == SIGMODE_CANCEL;
147 | bytes32 recoveryInfoHash = keccak256(abi.encode(recoveryInfo));
148 | require(privileges[signerKeyToRecover] == recoveryInfoHash, 'RECOVERY_NOT_AUTHORIZED');
149 |
150 | uint256 scheduled = scheduledRecoveries[hash];
151 | if (scheduled != 0 && !isCancellation) {
152 | require(block.timestamp > scheduled, 'RECOVERY_NOT_READY');
153 | nonce++;
154 | delete scheduledRecoveries[hash];
155 | emit LogRecoveryFinalized(hash, recoveryInfoHash, block.timestamp);
156 | } else {
157 | bytes32 hashToSign = isCancellation ? keccak256(abi.encode(hash, 0x63616E63)) : hash;
158 | address recoveryKey = SignatureValidator.recoverAddrImpl(hashToSign, innerRecoverySig, true);
159 | bool isIn;
160 | for (uint256 i = 0; i < recoveryInfo.keys.length; i++) {
161 | if (recoveryInfo.keys[i] == recoveryKey) {
162 | isIn = true;
163 | break;
164 | }
165 | }
166 | require(isIn, 'RECOVERY_NOT_AUTHORIZED');
167 | if (isCancellation) {
168 | delete scheduledRecoveries[hash];
169 | emit LogRecoveryCancelled(hash, recoveryInfoHash, recoveryKey, block.timestamp);
170 | } else {
171 | scheduledRecoveries[hash] = block.timestamp + recoveryInfo.timelock;
172 | emit LogRecoveryScheduled(hash, recoveryInfoHash, recoveryKey, currentNonce, block.timestamp, txns);
173 | }
174 | return;
175 | }
176 | } else {
177 | signerKey = SignatureValidator.recoverAddrImpl(hash, signature, true);
178 | require(privileges[signerKey] != bytes32(0), 'INSUFFICIENT_PRIVILEGE');
179 | }
180 |
181 | // we increment the nonce to prevent reentrancy
182 | // also, we do it here as we want to reuse the previous nonce
183 | // and respectively hash upon recovery / canceling
184 | // doing this after sig verification is fine because sig verification can only do STATICCALLS
185 | nonce = currentNonce + 1;
186 | executeBatch(txns);
187 |
188 | // The actual anti-bricking mechanism - do not allow a signerKey to drop their own privileges
189 | require(privileges[signerKey] != bytes32(0), 'PRIVILEGE_NOT_DOWNGRADED');
190 | }
191 |
192 | // @notice allows executing multiple bundles of calls (batch together multiple executes)
193 | function executeMultiple(ExecuteArgs[] calldata toExec) external payable {
194 | for (uint256 i = 0; i != toExec.length; i++) execute(toExec[i].txns, toExec[i].signature);
195 | }
196 |
197 | // @notice Allows executing calls if the caller itself is authorized
198 | // @dev no need for nonce management here cause we're not dealing with sigs
199 | function executeBySender(Transaction[] calldata txns) external payable {
200 | require(privileges[msg.sender] != bytes32(0), 'INSUFFICIENT_PRIVILEGE');
201 | executeBatch(txns);
202 | // again, anti-bricking
203 | require(privileges[msg.sender] != bytes32(0), 'PRIVILEGE_NOT_DOWNGRADED');
204 | }
205 |
206 | // @notice allows the contract itself to execute a batch of calls
207 | // self-calling is useful in cases like wanting to do multiple things in a tryCatchLimit
208 | function executeBySelf(Transaction[] calldata txns) external payable {
209 | require(msg.sender == address(this), 'ONLY_IDENTITY_CAN_CALL');
210 | executeBatch(txns);
211 | }
212 |
213 | function executeBatch(Transaction[] memory txns) internal {
214 | uint256 len = txns.length;
215 | for (uint256 i = 0; i < len; i++) {
216 | Transaction memory txn = txns[i];
217 | executeCall(txn.to, txn.value, txn.data);
218 | }
219 | }
220 |
221 | // we shouldn't use address.call(), cause: https://github.com/ethereum/solidity/issues/2884
222 | function executeCall(address to, uint256 value, bytes memory data) internal {
223 | assembly {
224 | let result := call(gas(), to, value, add(data, 0x20), mload(data), 0, 0)
225 |
226 | if eq(result, 0) {
227 | let size := returndatasize()
228 | let ptr := mload(0x40)
229 | returndatacopy(ptr, 0, size)
230 | revert(ptr, size)
231 | }
232 | }
233 | }
234 |
235 | // @notice EIP-1271 implementation
236 | // see https://eips.ethereum.org/EIPS/eip-1271
237 | function isValidSignature(bytes32 hash, bytes calldata signature) external view returns (bytes4) {
238 | if (privileges[SignatureValidator.recoverAddr(hash, signature)] != bytes32(0)) {
239 | // bytes4(keccak256("isValidSignature(bytes32,bytes)")
240 | return 0x1626ba7e;
241 | } else {
242 | return 0xffffffff;
243 | }
244 | }
245 |
246 | // @notice EIP-1155 implementation
247 | // we pretty much only need to signal that we support the interface for 165, but for 1155 we also need the fallback function
248 | function supportsInterface(bytes4 interfaceID) external pure returns (bool) {
249 | return
250 | interfaceID == 0x01ffc9a7 || // ERC-165 support (i.e. `bytes4(keccak256('supportsInterface(bytes4)'))`).
251 | interfaceID == 0x150b7a02 || // ERC721TokenReceiver
252 | interfaceID == 0x4e2312e0; // ERC-1155 `ERC1155TokenReceiver` support (i.e. `bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)")) ^ bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))`).
253 | }
254 | }
255 |
--------------------------------------------------------------------------------
/src/multisig/components/CoSign.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Button,
3 | Modal,
4 | ModalContent,
5 | ModalOverlay,
6 | FormControl,
7 | FormLabel,
8 | Input,
9 | useDisclosure,
10 | useToast,
11 | Alert,
12 | AlertIcon,
13 | Box,
14 | AlertTitle,
15 | AlertDescription,
16 | Text,
17 | Flex,
18 | } from "@chakra-ui/react";
19 | import QRCodeScanner from "../../common/QRCodeScanner";
20 | import { useState, useContext } from "react";
21 | import Schnorrkel, { Key, Signature } from "@borislav.itskov/schnorrkel.js";
22 | import { computeSchnorrAddress, getDeployCalldata, getExecuteCalldata, getMultisigAddress, wrapSchnorr } from "../../utils/helpers";
23 | import { ethers } from "ethers";
24 | import MultisigContext from "../../auth/context/multisig";
25 |
26 | import { FACTORY_ADDRESS, mainProvider, ENTRY_POINT_ADDRESS } from "../../config/constants";
27 | import { useForm } from "react-hook-form";
28 | import { getSchnorrkelInstance } from "../../singletons/Schnorr";
29 | import { useEOA } from "../../auth/context/eoa";
30 | import { useSteps } from "../../auth/context/step";
31 | import { EntryPoint__factory } from "@account-abstraction/contracts";
32 | import ERC4337Account from '../../builds/ERC4337Account.json'
33 |
34 | interface FormProps {
35 | to: string;
36 | value: number;
37 | gasPrice: string;
38 | }
39 |
40 | const CoSign = (props: any) => {
41 | const toast = useToast()
42 | const { setActiveStep } = useSteps()
43 | const { eoaPrivateKey, eoaPublicKey } = useEOA()
44 | const { createAndStoreMultisigDataIfNeeded, getAllMultisigData } = useContext(MultisigContext)
45 | const { isOpen, onOpen, onClose } = useDisclosure();
46 | const { isOpen: isFormOpen, onOpen: onFormOpen, onClose: onFormClose } = useDisclosure();
47 | const [transactionHash, setTransactionHash] = useState(null)
48 | const [paymasterAndData, setPaymasterAndData] = useState(null)
49 |
50 | const {
51 | handleSubmit,
52 | register,
53 | formState: { isSubmitting },
54 | setValue,
55 | } = useForm();
56 | const handleScanSuccess = (scan: any = "") => {
57 | const data = scan.split("|");
58 |
59 | // TODO: Validate better if data is multisig!
60 | if (data.length !== 8) {
61 | alert("Missing all multisig data in the QR code you scanned!");
62 |
63 | return;
64 | }
65 |
66 | const publicKey = eoaPublicKey;
67 | const multisigPartnerPublicKey = data[0];
68 | const multisigPartnerKPublicHex = data[1];
69 | const multisigPartnerKTwoPublicHex = data[2];
70 | const multisigPartnerSignature = data[3];
71 | const formTo = data[4];
72 | const formValue = data[5];
73 | const gasPrice = data[6];
74 | const pimlicoPaymaster = data[7];
75 |
76 | const publicKeyOne = new Key(Buffer.from(ethers.utils.arrayify(publicKey)));
77 | const publicKeyTwo = new Key(
78 | Buffer.from(ethers.utils.arrayify(multisigPartnerPublicKey))
79 | );
80 | const combinedPublicKey = Schnorrkel.getCombinedPublicKey([
81 | publicKeyOne,
82 | publicKeyTwo,
83 | ]);
84 | const multisigAddr = getMultisigAddress([publicKeyOne, publicKeyTwo])
85 |
86 | // Set data in local storage
87 | createAndStoreMultisigDataIfNeeded({
88 | "multisigPartnerPublicKey": multisigPartnerPublicKey,
89 | "multisigPartnerKPublicHex": multisigPartnerKPublicHex,
90 | "multisigPartnerKTwoPublicHex": multisigPartnerKTwoPublicHex,
91 | "multisigAddr": multisigAddr,
92 | "multisigPartnerSignature": multisigPartnerSignature,
93 | "combinedPublicKey": combinedPublicKey
94 | })
95 |
96 | onClose();
97 | setValue('to', formTo);
98 | setValue('value', +formValue);
99 | setValue('gasPrice', gasPrice);
100 | setPaymasterAndData(pimlicoPaymaster)
101 | onFormOpen();
102 | };
103 | const handleScanError = (error: any) => console.error(error);
104 |
105 | // sign and submit the transaction
106 | const onSubmit = async (values: FormProps) => {
107 | const data = getAllMultisigData();
108 | if (!data) return
109 |
110 | const abiCoder = new ethers.utils.AbiCoder();
111 | const sendTosignerTxn = [
112 | values.to,
113 | ethers.utils.parseEther(values.value.toString()),
114 | "0x00",
115 | ];
116 | const txns = [sendTosignerTxn];
117 | const publicKeyOne = new Key(
118 | Buffer.from(ethers.utils.arrayify(eoaPublicKey))
119 | );
120 | const publicKeyTwo = new Key(
121 | Buffer.from(ethers.utils.arrayify(data.multisigPartnerPublicKey))
122 | );
123 | const publicKeys = [publicKeyOne, publicKeyTwo];
124 | const privateKey = new Key(
125 | Buffer.from(ethers.utils.arrayify(eoaPrivateKey))
126 | );
127 | const partnerNonces = {
128 | kPublic: Key.fromHex(data.multisigPartnerKPublicHex),
129 | kTwoPublic: Key.fromHex(data.multisigPartnerKTwoPublicHex),
130 | };
131 | const schnorrkel = getSchnorrkelInstance()
132 | const publicNonces = schnorrkel.getPublicNonces(privateKey)
133 | const combinedPublicNonces = [publicNonces, partnerNonces];
134 |
135 | const entryPoint = EntryPoint__factory.connect(ENTRY_POINT_ADDRESS, mainProvider)
136 | const entryPointNonce = await entryPoint.getNonce(data.multisigAddr, 0)
137 | const schnorrVirtualAddr = computeSchnorrAddress(data.combinedPublicKey)
138 | const userOpNonce = entryPointNonce.toHexString()
139 | const bytecodeWithArgs = ethers.utils.concat([
140 | ERC4337Account.bytecode,
141 | abiCoder.encode(['address', 'address[]'], [ENTRY_POINT_ADDRESS, [schnorrVirtualAddr]])
142 | ])
143 | const initCode = ethers.utils.hexlify(ethers.utils.concat([
144 | FACTORY_ADDRESS,
145 | getDeployCalldata(bytecodeWithArgs)
146 | ]))
147 | const executeCalldata = getExecuteCalldata([txns])
148 | const userOperation = {
149 | sender: data.multisigAddr,
150 | nonce: userOpNonce,
151 | initCode,
152 | callData: executeCalldata,
153 | callGasLimit: ethers.utils.hexlify(100_000), // hardcode it for now at a high value
154 | verificationGasLimit: ethers.utils.hexlify(2_000_000), // hardcode it for now at a high value
155 | preVerificationGas: ethers.utils.hexlify(50_000), // hardcode it for now at a high value
156 | maxFeePerGas: values.gasPrice,
157 | maxPriorityFeePerGas: values.gasPrice,
158 | paymasterAndData: paymasterAndData,
159 | signature: "0x"
160 | }
161 | const userOpHash = await entryPoint.getUserOpHash(userOperation)
162 | const { signature, challenge, finalPublicNonce } = schnorrkel.multiSigSignHash(
163 | privateKey,
164 | userOpHash,
165 | publicKeys,
166 | combinedPublicNonces
167 | );
168 | const partnerSig = Signature.fromHex(data.multisigPartnerSignature)
169 | const summedSig = Schnorrkel.sumSigs([signature, partnerSig])
170 | const verification = Schnorrkel.verifyHash(summedSig, userOpHash, finalPublicNonce, data.combinedPublicKey)
171 | console.log('VERIFICATION: ' + verification)
172 |
173 | // set the user op signature
174 | const px = ethers.utils.hexlify(data.combinedPublicKey.buffer.slice(1, 33))
175 | const parity = data.combinedPublicKey.buffer[0] - 2 + 27
176 | const sigDataUserOp = abiCoder.encode([ 'bytes32', 'bytes32', 'bytes32', 'uint8' ], [
177 | px,
178 | challenge.buffer,
179 | summedSig.buffer,
180 | parity
181 | ])
182 | const wrappedSig = wrapSchnorr(sigDataUserOp)
183 | userOperation.signature = wrappedSig
184 |
185 | // send the transaction
186 | const apiKey = process.env.REACT_APP_PIMLICO_API_KEY
187 | const pimlicoEndpoint = `https://api.pimlico.io/v1/polygon/rpc?apikey=${apiKey}`
188 | const pimlicoProvider = new ethers.providers.StaticJsonRpcProvider(pimlicoEndpoint)
189 | const userOperationHash = await pimlicoProvider.send("eth_sendUserOperation", [userOperation, ENTRY_POINT_ADDRESS])
190 |
191 | // let's also wait for the userOperation to be included, by continually querying for the receipts
192 | console.log("Querying for receipts...")
193 | let receipt = null
194 | let counter = 0
195 | while (receipt === null) {
196 | try {
197 | await new Promise((r) => setTimeout(r, 1000)) //sleep
198 | counter++
199 | receipt = await pimlicoProvider.send("eth_getUserOperationReceipt", [userOperationHash])
200 | console.log(receipt)
201 | } catch (e) {
202 | console.log('error throwed, retry counter ' + counter)
203 | }
204 | }
205 |
206 | const txHash = receipt.receipt.transactionHash
207 | console.log(`${txHash}`)
208 |
209 | onFormClose()
210 | toast({
211 | title: 'Successfully signed!',
212 | position: 'top',
213 | status: 'success',
214 | duration: 9000,
215 | isClosable: true,
216 | })
217 | setActiveStep(3)
218 |
219 | setTransactionHash(txHash)
220 | }
221 |
222 | const openInExplorer = () => {
223 | if (!transactionHash) return;
224 |
225 | const polygonScanUrl = `https://polygonscan.com/tx/${transactionHash}`;
226 | window.open(polygonScanUrl, '_blank');
227 | }
228 |
229 | return (
230 | <>
231 |
232 |
233 | {transactionHash && (
234 |
235 |
236 |
237 | Transaction Sent! Hash:
238 |
239 | {transactionHash}
240 |
243 |
244 |
245 |
246 | )}
247 |
248 |
249 |
250 |
251 |
255 |
256 |
257 |
258 |
259 |
260 | Co-sign Transaction
261 |
300 |
301 |
302 | >
303 | );
304 | };
305 |
306 | export default CoSign;
307 |
--------------------------------------------------------------------------------
/src/builds/ERC4337Account.json:
--------------------------------------------------------------------------------
1 | {
2 | "_format": "hh-sol-artifact-1",
3 | "contractName": "ERC4337Account",
4 | "sourceName": "contracts/ERC4337Account.sol",
5 | "abi": [
6 | {
7 | "inputs": [
8 | {
9 | "internalType": "address",
10 | "name": "_entryPoint",
11 | "type": "address"
12 | },
13 | {
14 | "internalType": "address[]",
15 | "name": "addrs",
16 | "type": "address[]"
17 | }
18 | ],
19 | "stateMutability": "nonpayable",
20 | "type": "constructor"
21 | },
22 | {
23 | "anonymous": false,
24 | "inputs": [
25 | {
26 | "indexed": true,
27 | "internalType": "address",
28 | "name": "to",
29 | "type": "address"
30 | },
31 | {
32 | "indexed": false,
33 | "internalType": "uint256",
34 | "name": "value",
35 | "type": "uint256"
36 | },
37 | {
38 | "indexed": false,
39 | "internalType": "bytes",
40 | "name": "data",
41 | "type": "bytes"
42 | },
43 | {
44 | "indexed": false,
45 | "internalType": "bytes",
46 | "name": "returnData",
47 | "type": "bytes"
48 | }
49 | ],
50 | "name": "LogErr",
51 | "type": "event"
52 | },
53 | {
54 | "anonymous": false,
55 | "inputs": [
56 | {
57 | "indexed": true,
58 | "internalType": "address",
59 | "name": "addr",
60 | "type": "address"
61 | },
62 | {
63 | "indexed": false,
64 | "internalType": "bytes32",
65 | "name": "priv",
66 | "type": "bytes32"
67 | }
68 | ],
69 | "name": "LogPrivilegeChanged",
70 | "type": "event"
71 | },
72 | {
73 | "anonymous": false,
74 | "inputs": [
75 | {
76 | "indexed": true,
77 | "internalType": "bytes32",
78 | "name": "txnHash",
79 | "type": "bytes32"
80 | },
81 | {
82 | "indexed": true,
83 | "internalType": "bytes32",
84 | "name": "recoveryHash",
85 | "type": "bytes32"
86 | },
87 | {
88 | "indexed": true,
89 | "internalType": "address",
90 | "name": "recoveryKey",
91 | "type": "address"
92 | },
93 | {
94 | "indexed": false,
95 | "internalType": "uint256",
96 | "name": "time",
97 | "type": "uint256"
98 | }
99 | ],
100 | "name": "LogRecoveryCancelled",
101 | "type": "event"
102 | },
103 | {
104 | "anonymous": false,
105 | "inputs": [
106 | {
107 | "indexed": true,
108 | "internalType": "bytes32",
109 | "name": "txnHash",
110 | "type": "bytes32"
111 | },
112 | {
113 | "indexed": true,
114 | "internalType": "bytes32",
115 | "name": "recoveryHash",
116 | "type": "bytes32"
117 | },
118 | {
119 | "indexed": false,
120 | "internalType": "uint256",
121 | "name": "time",
122 | "type": "uint256"
123 | }
124 | ],
125 | "name": "LogRecoveryFinalized",
126 | "type": "event"
127 | },
128 | {
129 | "anonymous": false,
130 | "inputs": [
131 | {
132 | "indexed": true,
133 | "internalType": "bytes32",
134 | "name": "txnHash",
135 | "type": "bytes32"
136 | },
137 | {
138 | "indexed": true,
139 | "internalType": "bytes32",
140 | "name": "recoveryHash",
141 | "type": "bytes32"
142 | },
143 | {
144 | "indexed": true,
145 | "internalType": "address",
146 | "name": "recoveryKey",
147 | "type": "address"
148 | },
149 | {
150 | "indexed": false,
151 | "internalType": "uint256",
152 | "name": "nonce",
153 | "type": "uint256"
154 | },
155 | {
156 | "indexed": false,
157 | "internalType": "uint256",
158 | "name": "time",
159 | "type": "uint256"
160 | },
161 | {
162 | "components": [
163 | {
164 | "internalType": "address",
165 | "name": "to",
166 | "type": "address"
167 | },
168 | {
169 | "internalType": "uint256",
170 | "name": "value",
171 | "type": "uint256"
172 | },
173 | {
174 | "internalType": "bytes",
175 | "name": "data",
176 | "type": "bytes"
177 | }
178 | ],
179 | "indexed": false,
180 | "internalType": "struct AmbireAccount.Transaction[]",
181 | "name": "txns",
182 | "type": "tuple[]"
183 | }
184 | ],
185 | "name": "LogRecoveryScheduled",
186 | "type": "event"
187 | },
188 | {
189 | "stateMutability": "payable",
190 | "type": "fallback"
191 | },
192 | {
193 | "inputs": [],
194 | "name": "entryPoint",
195 | "outputs": [
196 | {
197 | "internalType": "address",
198 | "name": "",
199 | "type": "address"
200 | }
201 | ],
202 | "stateMutability": "view",
203 | "type": "function"
204 | },
205 | {
206 | "inputs": [
207 | {
208 | "components": [
209 | {
210 | "internalType": "address",
211 | "name": "to",
212 | "type": "address"
213 | },
214 | {
215 | "internalType": "uint256",
216 | "name": "value",
217 | "type": "uint256"
218 | },
219 | {
220 | "internalType": "bytes",
221 | "name": "data",
222 | "type": "bytes"
223 | }
224 | ],
225 | "internalType": "struct AmbireAccount.Transaction[]",
226 | "name": "txns",
227 | "type": "tuple[]"
228 | },
229 | {
230 | "internalType": "bytes",
231 | "name": "signature",
232 | "type": "bytes"
233 | }
234 | ],
235 | "name": "execute",
236 | "outputs": [],
237 | "stateMutability": "payable",
238 | "type": "function"
239 | },
240 | {
241 | "inputs": [
242 | {
243 | "components": [
244 | {
245 | "internalType": "address",
246 | "name": "to",
247 | "type": "address"
248 | },
249 | {
250 | "internalType": "uint256",
251 | "name": "value",
252 | "type": "uint256"
253 | },
254 | {
255 | "internalType": "bytes",
256 | "name": "data",
257 | "type": "bytes"
258 | }
259 | ],
260 | "internalType": "struct AmbireAccount.Transaction[]",
261 | "name": "txns",
262 | "type": "tuple[]"
263 | }
264 | ],
265 | "name": "executeBySelf",
266 | "outputs": [],
267 | "stateMutability": "payable",
268 | "type": "function"
269 | },
270 | {
271 | "inputs": [
272 | {
273 | "components": [
274 | {
275 | "internalType": "address",
276 | "name": "to",
277 | "type": "address"
278 | },
279 | {
280 | "internalType": "uint256",
281 | "name": "value",
282 | "type": "uint256"
283 | },
284 | {
285 | "internalType": "bytes",
286 | "name": "data",
287 | "type": "bytes"
288 | }
289 | ],
290 | "internalType": "struct AmbireAccount.Transaction[]",
291 | "name": "txns",
292 | "type": "tuple[]"
293 | }
294 | ],
295 | "name": "executeBySender",
296 | "outputs": [],
297 | "stateMutability": "payable",
298 | "type": "function"
299 | },
300 | {
301 | "inputs": [
302 | {
303 | "components": [
304 | {
305 | "components": [
306 | {
307 | "internalType": "address",
308 | "name": "to",
309 | "type": "address"
310 | },
311 | {
312 | "internalType": "uint256",
313 | "name": "value",
314 | "type": "uint256"
315 | },
316 | {
317 | "internalType": "bytes",
318 | "name": "data",
319 | "type": "bytes"
320 | }
321 | ],
322 | "internalType": "struct AmbireAccount.Transaction[]",
323 | "name": "txns",
324 | "type": "tuple[]"
325 | },
326 | {
327 | "internalType": "bytes",
328 | "name": "signature",
329 | "type": "bytes"
330 | }
331 | ],
332 | "internalType": "struct AmbireAccount.ExecuteArgs[]",
333 | "name": "toExec",
334 | "type": "tuple[]"
335 | }
336 | ],
337 | "name": "executeMultiple",
338 | "outputs": [],
339 | "stateMutability": "payable",
340 | "type": "function"
341 | },
342 | {
343 | "inputs": [
344 | {
345 | "internalType": "bytes32",
346 | "name": "hash",
347 | "type": "bytes32"
348 | },
349 | {
350 | "internalType": "bytes",
351 | "name": "signature",
352 | "type": "bytes"
353 | }
354 | ],
355 | "name": "isValidSignature",
356 | "outputs": [
357 | {
358 | "internalType": "bytes4",
359 | "name": "",
360 | "type": "bytes4"
361 | }
362 | ],
363 | "stateMutability": "view",
364 | "type": "function"
365 | },
366 | {
367 | "inputs": [],
368 | "name": "nonce",
369 | "outputs": [
370 | {
371 | "internalType": "uint256",
372 | "name": "",
373 | "type": "uint256"
374 | }
375 | ],
376 | "stateMutability": "view",
377 | "type": "function"
378 | },
379 | {
380 | "inputs": [
381 | {
382 | "internalType": "address",
383 | "name": "",
384 | "type": "address"
385 | },
386 | {
387 | "internalType": "address",
388 | "name": "",
389 | "type": "address"
390 | },
391 | {
392 | "internalType": "uint256[]",
393 | "name": "",
394 | "type": "uint256[]"
395 | },
396 | {
397 | "internalType": "uint256[]",
398 | "name": "",
399 | "type": "uint256[]"
400 | },
401 | {
402 | "internalType": "bytes",
403 | "name": "",
404 | "type": "bytes"
405 | }
406 | ],
407 | "name": "onERC1155BatchReceived",
408 | "outputs": [
409 | {
410 | "internalType": "bytes4",
411 | "name": "",
412 | "type": "bytes4"
413 | }
414 | ],
415 | "stateMutability": "pure",
416 | "type": "function"
417 | },
418 | {
419 | "inputs": [
420 | {
421 | "internalType": "address",
422 | "name": "",
423 | "type": "address"
424 | },
425 | {
426 | "internalType": "address",
427 | "name": "",
428 | "type": "address"
429 | },
430 | {
431 | "internalType": "uint256",
432 | "name": "",
433 | "type": "uint256"
434 | },
435 | {
436 | "internalType": "uint256",
437 | "name": "",
438 | "type": "uint256"
439 | },
440 | {
441 | "internalType": "bytes",
442 | "name": "",
443 | "type": "bytes"
444 | }
445 | ],
446 | "name": "onERC1155Received",
447 | "outputs": [
448 | {
449 | "internalType": "bytes4",
450 | "name": "",
451 | "type": "bytes4"
452 | }
453 | ],
454 | "stateMutability": "pure",
455 | "type": "function"
456 | },
457 | {
458 | "inputs": [
459 | {
460 | "internalType": "address",
461 | "name": "",
462 | "type": "address"
463 | },
464 | {
465 | "internalType": "address",
466 | "name": "",
467 | "type": "address"
468 | },
469 | {
470 | "internalType": "uint256",
471 | "name": "",
472 | "type": "uint256"
473 | },
474 | {
475 | "internalType": "bytes",
476 | "name": "",
477 | "type": "bytes"
478 | }
479 | ],
480 | "name": "onERC721Received",
481 | "outputs": [
482 | {
483 | "internalType": "bytes4",
484 | "name": "",
485 | "type": "bytes4"
486 | }
487 | ],
488 | "stateMutability": "pure",
489 | "type": "function"
490 | },
491 | {
492 | "inputs": [
493 | {
494 | "internalType": "address",
495 | "name": "",
496 | "type": "address"
497 | }
498 | ],
499 | "name": "privileges",
500 | "outputs": [
501 | {
502 | "internalType": "bytes32",
503 | "name": "",
504 | "type": "bytes32"
505 | }
506 | ],
507 | "stateMutability": "view",
508 | "type": "function"
509 | },
510 | {
511 | "inputs": [
512 | {
513 | "internalType": "bytes32",
514 | "name": "",
515 | "type": "bytes32"
516 | }
517 | ],
518 | "name": "scheduledRecoveries",
519 | "outputs": [
520 | {
521 | "internalType": "uint256",
522 | "name": "",
523 | "type": "uint256"
524 | }
525 | ],
526 | "stateMutability": "view",
527 | "type": "function"
528 | },
529 | {
530 | "inputs": [
531 | {
532 | "internalType": "address",
533 | "name": "addr",
534 | "type": "address"
535 | },
536 | {
537 | "internalType": "bytes32",
538 | "name": "priv",
539 | "type": "bytes32"
540 | }
541 | ],
542 | "name": "setAddrPrivilege",
543 | "outputs": [],
544 | "stateMutability": "payable",
545 | "type": "function"
546 | },
547 | {
548 | "inputs": [
549 | {
550 | "internalType": "bytes4",
551 | "name": "interfaceID",
552 | "type": "bytes4"
553 | }
554 | ],
555 | "name": "supportsInterface",
556 | "outputs": [
557 | {
558 | "internalType": "bool",
559 | "name": "",
560 | "type": "bool"
561 | }
562 | ],
563 | "stateMutability": "pure",
564 | "type": "function"
565 | },
566 | {
567 | "inputs": [
568 | {
569 | "internalType": "address",
570 | "name": "to",
571 | "type": "address"
572 | },
573 | {
574 | "internalType": "uint256",
575 | "name": "value",
576 | "type": "uint256"
577 | },
578 | {
579 | "internalType": "bytes",
580 | "name": "data",
581 | "type": "bytes"
582 | }
583 | ],
584 | "name": "tryCatch",
585 | "outputs": [],
586 | "stateMutability": "payable",
587 | "type": "function"
588 | },
589 | {
590 | "inputs": [
591 | {
592 | "internalType": "address",
593 | "name": "to",
594 | "type": "address"
595 | },
596 | {
597 | "internalType": "uint256",
598 | "name": "value",
599 | "type": "uint256"
600 | },
601 | {
602 | "internalType": "bytes",
603 | "name": "data",
604 | "type": "bytes"
605 | },
606 | {
607 | "internalType": "uint256",
608 | "name": "gasLimit",
609 | "type": "uint256"
610 | }
611 | ],
612 | "name": "tryCatchLimit",
613 | "outputs": [],
614 | "stateMutability": "payable",
615 | "type": "function"
616 | },
617 | {
618 | "inputs": [
619 | {
620 | "components": [
621 | {
622 | "internalType": "address",
623 | "name": "sender",
624 | "type": "address"
625 | },
626 | {
627 | "internalType": "uint256",
628 | "name": "nonce",
629 | "type": "uint256"
630 | },
631 | {
632 | "internalType": "bytes",
633 | "name": "initCode",
634 | "type": "bytes"
635 | },
636 | {
637 | "internalType": "bytes",
638 | "name": "callData",
639 | "type": "bytes"
640 | },
641 | {
642 | "internalType": "uint256",
643 | "name": "callGasLimit",
644 | "type": "uint256"
645 | },
646 | {
647 | "internalType": "uint256",
648 | "name": "verificationGasLimit",
649 | "type": "uint256"
650 | },
651 | {
652 | "internalType": "uint256",
653 | "name": "preVerificationGas",
654 | "type": "uint256"
655 | },
656 | {
657 | "internalType": "uint256",
658 | "name": "maxFeePerGas",
659 | "type": "uint256"
660 | },
661 | {
662 | "internalType": "uint256",
663 | "name": "maxPriorityFeePerGas",
664 | "type": "uint256"
665 | },
666 | {
667 | "internalType": "bytes",
668 | "name": "paymasterAndData",
669 | "type": "bytes"
670 | },
671 | {
672 | "internalType": "bytes",
673 | "name": "signature",
674 | "type": "bytes"
675 | }
676 | ],
677 | "internalType": "struct UserOperation",
678 | "name": "userOp",
679 | "type": "tuple"
680 | },
681 | {
682 | "internalType": "bytes32",
683 | "name": "userOpHash",
684 | "type": "bytes32"
685 | },
686 | {
687 | "internalType": "uint256",
688 | "name": "missingAccountFunds",
689 | "type": "uint256"
690 | }
691 | ],
692 | "name": "validateUserOp",
693 | "outputs": [
694 | {
695 | "internalType": "uint256",
696 | "name": "validationData",
697 | "type": "uint256"
698 | }
699 | ],
700 | "stateMutability": "nonpayable",
701 | "type": "function"
702 | },
703 | {
704 | "stateMutability": "payable",
705 | "type": "receive"
706 | }
707 | ],
708 | "bytecode": "0x604060a08152346200017c576200224990813803806200001f8162000197565b938439820181838203126200017c576200003983620001bd565b6020848101516001600160401b03959193918682116200017c570181601f820112156200017c57805195861162000181578560051b9084806200007e81850162000197565b8099815201928201019283116200017c5784809101915b8383106200016157505050508060805260018060a01b0390816000911681528083526001928385832055855192825b848110620000eb57865161204b9081620001fe823960805181818161047a01526106f30152f35b81620000f8828a620001d2565b5116845283835285878520558162000111828a620001d2565b51167f08ac40e0195c5998554e98853c23964a13a24309e127b2d016e8ec4adecf5e83848951898152a260001981146200014d578501620000c4565b634e487b7160e01b84526011600452602484fd5b81906200016e84620001bd565b815201910190849062000095565b600080fd5b634e487b7160e01b600052604160045260246000fd5b6040519190601f01601f191682016001600160401b038111838210176200018157604052565b51906001600160a01b03821682036200017c57565b8051821015620001e75760209160051b010190565b634e487b7160e01b600052603260045260246000fdfe6080604052600436101561001e575b361561001c5761001c6108da565b005b60003560e01c806301ffc9a71461012e5780630d5828d414610129578063150b7a0214610124578063160aa0ef1461011f5780631626ba7e1461011a5780633628d042146101155780633a871cdd146101105780636171d1c91461010b5780636769de8214610106578063a2ea676614610101578063abc5345e146100fc578063affed0e0146100f7578063b0d691fe146100f2578063bc197c81146100ed578063bf1cb383146100e8578063c066a5b1146100e35763f23a6e610361000e57610867565b610829565b6107c0565b610717565b6106d3565b6106b5565b610672565b6105f2565b6105cc565b61054c565b610422565b61036d565b61031c565b6102f0565b610296565b6101fc565b61014a565b6001600160e01b031981160361014557565b600080fd5b346101455760203660031901126101455760206001600160e01b031960043561017281610133565b167f01ffc9a70000000000000000000000000000000000000000000000000000000081149081156101da575b81156101b0575b506040519015158152f35b7f4e2312e000000000000000000000000000000000000000000000000000000000915014386101a5565b630a85bd0160e11b8114915061019e565b6001600160a01b0381160361014557565b604036600319011261014557600435610214816101eb565b7f08ac40e0195c5998554e98853c23964a13a24309e127b2d016e8ec4adecf5e8360206001600160a01b036024359361024e30331461091d565b1692836000526000825280604060002055604051908152a2005b9181601f840112156101455782359167ffffffffffffffff8311610145576020838186019501011161014557565b34610145576080366003190112610145576102b26004356101eb565b6102bd6024356101eb565b60643567ffffffffffffffff8111610145576102dd903690600401610268565b50506020604051630a85bd0160e11b8152f35b346101455760203660031901126101455760043560005260026020526020604060002054604051908152f35b346101455760403660031901126101455760243567ffffffffffffffff81116101455761035b6103526020923690600401610268565b906004356114a6565b6001600160e01b031960405191168152f35b608036600319011261014557600435610385816101eb565b6024359060443567ffffffffffffffff8111610145576103a9903690600401610268565b90916103b630331461091d565b600080604051848682378085810183815203908785606435f1926103d86109f9565b93156103e057005b7f80c2637928bd94d0e1a90eaac1efc1553be6391d962fe5c82a01e90122c84ccc936001600160a01b039361041d92604051958695169785610a92565b0390a2005b3461014557600319606036820112610145576004359067ffffffffffffffff821161014557610160908236030112610145576104af6104f5916104d76104be604435926104b660009586926104a16001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001633146114ef565b610144810190600401611415565b3691610c21565b6024356119c5565b6001600160a01b03166000526000602052604060002090565b5415610512575b806104f9575b506040519081529081906020820190565b0390f35b600080808093335af15061050b6109f9565b50386104e4565b600191506104de565b9181601f840112156101455782359167ffffffffffffffff8311610145576020808501948460051b01011161014557565b60403660031901126101455767ffffffffffffffff6004358181116101455761057990369060040161051b565b906024359283116101455761059561001c933690600401610268565b929091611030565b6020600319820112610145576004359067ffffffffffffffff8211610145576105c89160040161051b565b9091565b61001c6105ed6105db3661059d565b6105e630331461091d565b3691610f24565b611448565b6105fb3661059d565b60005b81810361060757005b6106128183856113f3565b803590601e198136030182121561014557019081359167ffffffffffffffff8311610145576020809101928060051b360384136101455761066d936105956106689361065f86898b6113f3565b90810190611415565b610e7f565b6105fe565b6105ed61069c6106813661059d565b9290600093338552846020526105e660408620541515610c58565b338152806020526106b260408220541515610fe5565b80f35b34610145576000366003190112610145576020600154604051908152f35b346101455760003660031901126101455760206040516001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000168152f35b346101455760a0366003190112610145576107336004356101eb565b61073e6024356101eb565b67ffffffffffffffff6044358181116101455761075f90369060040161051b565b50506064358181116101455761077990369060040161051b565b505060843590811161014557610793903690600401610268565b50506040517fbc197c81000000000000000000000000000000000000000000000000000000008152602090f35b6060366003190112610145576004356107d8816101eb565b6024359060443567ffffffffffffffff8111610145576107fc903690600401610268565b909161080930331461091d565b6000805a604051858782378785828881018681520393f1926103d86109f9565b34610145576020366003190112610145576001600160a01b0360043561084e816101eb565b1660005260006020526020604060002054604051908152f35b346101455760a0366003190112610145576108836004356101eb565b61088e6024356101eb565b60843567ffffffffffffffff8111610145576108ae903690600401610268565b505060206040517ff23a6e61000000000000000000000000000000000000000000000000000000008152f35b6000906169698252816020526001600160a01b036040832054169081156109185750818091368280378136915af4903d918282803e1561091657f35bfd5b915050565b1561092457565b606460405162461bcd60e51b815260206004820152601660248201527f4f4e4c595f4944454e544954595f43414e5f43414c4c000000000000000000006044820152fd5b634e487b7160e01b600052604160045260246000fd5b6040810190811067ffffffffffffffff82111761099a57604052565b610968565b6060810190811067ffffffffffffffff82111761099a57604052565b90601f8019910116810190811067ffffffffffffffff82111761099a57604052565b67ffffffffffffffff811161099a57601f01601f191660200190565b3d15610a24573d90610a0a826109dd565b91610a1860405193846109bb565b82523d6000602084013e565b606090565b908060209392818452848401376000828201840152601f01601f1916010190565b60005b838110610a5d5750506000910152565b8181015183820152602001610a4d565b90602091610a8681518092818552858086019101610a4a565b601f01601f1916010190565b9291610abe9492610ab0928552606060208601526060850191610a29565b916040818403910152610a6d565b90565b90808352602080930192838260051b850194846000925b858410610ae9575050505050505090565b9091929394959681810384528735605e19843603018112156101455783016001600160a01b038135610b1a816101eb565b168252868101358783015260409081810135601e198236030181121561014557019087823592019267ffffffffffffffff83116101455782360384136101455760019389938493610b7393606080928201520191610a29565b990194019401929594939190610ad8565b90926080926001600160a01b03610abe9795168352602083015260408201528160608201520191610ac1565b634e487b7160e01b600052601160045260246000fd5b600019810191908211610bd557565b610bb0565b9070014551231950b75fc4402da1732fc9bebe19918203918211610bd557565b634e487b7160e01b600052603260045260246000fd5b90821015610c1c570190565b610bfa565b929192610c2d826109dd565b91610c3b60405193846109bb565b829481845281830111610145578281602093846000960137010152565b15610c5f57565b606460405162461bcd60e51b815260206004820152601660248201527f494e53554646494349454e545f50524956494c454745000000000000000000006044820152fd5b67ffffffffffffffff811161099a5760051b60200190565b81601f82011215610145578051610cd1816109dd565b92610cdf60405194856109bb565b8184526020828401011161014557610abe9160208085019101610a4a565b5190610d08826101eb565b565b909160608284031261014557815167ffffffffffffffff908181116101455783019360408582031261014557604051610d428161097e565b855183811161014557860182601f8201121561014557805196610d6488610ca3565b610d7160405191826109bb565b88815260209860051b83018901898201868211610145578a80809601915b838310610dc5575050505083520151868201529484015191821161014557610dbe604091610abe938601610cbb565b9301610cfd565b81908351610dd2816101eb565b8152019101908590610d8f565b6020808252825160408383015280516060840181905260808401949183019060005b818110610e175750505090604091015191015290565b82516001600160a01b031687529584019591840191600101610e01565b15610e3b57565b606460405162461bcd60e51b815260206004820152601760248201527f5245434f564552595f4e4f545f415554484f52495a45440000000000000000006044820152fd5b6000198114610bd55760010190565b8051821015610c1c5760209160051b010190565b9060018201809211610bd557565b91908201809211610bd557565b610abe9492606092825260208201528160408201520191610ac1565b15610ee057565b606460405162461bcd60e51b815260206004820152601260248201527f5245434f564552595f4e4f545f524541445900000000000000000000000000006044820152fd5b929190610f3081610ca3565b91604091610f40835194856109bb565b839581855260208095019160051b8101938385116101455781925b858410610f6b5750505050505050565b67ffffffffffffffff9084358281116101455784019160608388031261014557835192610f978461099f565b8035610fa2816101eb565b8452898101358a8501528481013591821161014557019086601f83011215610145578892610fd68884868096359101610c21565b85820152815201930192610f5b565b15610fec57565b606460405162461bcd60e51b815260206004820152601860248201527f50524956494c4547455f4e4f545f444f574e47524144454400000000000000006044820152fd5b9192600190815491604091825190602080830183611052898b8a463087610b84565b0393611066601f19958681018352826109bb565b5190209860ff6110b16110ab61108561107e87610bc6565b8786610c10565b357fff000000000000000000000000000000000000000000000000000000000000001690565b60f81c90565b169260ff841480156113e9575b1561138757916110f46110e56110e060fe9d9896946111489a98963691610c21565b61155b565b50828082518301019101610d0a565b939061114f6001600160a01b038096169e8f9314928951868101906111298161111d8985610ddf565b038b81018352826109bb565b5190209b8c916001600160a01b03166000526000602052604060002090565b5414610e34565b611163896000526002602052604060002090565b54958615158061137f575b156111f8575050505050506104be956105ed956111f099957fdef2c5a081ab1dd3f896874500de5f4045efd102a452bce3af5123459f461f1b610d089c9a966111d06111cb6105e6986111c56111cb994211610ed9565b54610e7f565b600155565b6000838152600260205260408120555142815280602081010390a3610ea2565b541515610fe5565b61124c93959e50879d999b989a9c97929496508b9085600014611373578b518881019283526363616e6360208401529061123e9082604085015b039081018352826109bb565b51902090829e9392916119c5565b6000918291169b5b611314575b5061126e9192939495969798999a9b50610e34565b156112c05750505050507ff38fc5203ec5ac432b66df0ed37402f7b1afb377ddce98e08c2bf696c71874059060006112b0846000526002602052604060002090565b555142815280602081015b0390a4565b946112f36112bb93927f58d616db4bbaccaaad10b948d290b9df7973a7b566f83a68e2e0e7f4e5c61ccd97015142610eb0565b611307886000526002602052604060002090565b5551938493429085610ebd565b8351805182101561136d5761134a61133e6113318f938590610e8e565b516001600160a01b031690565b6001600160a01b031690565b1461135f576113598d91610e7f565b90611254565b50999a8b9a5061126e611259565b50611259565b5090829e9392916119c5565b50821561116e565b6105ed96506104be979894506105e693506111cb925094610d089a6113b56113bb936111f09c983691610c21565b906119c5565b966113e46113dc896001600160a01b03166000526000602052604060002090565b541515610c58565b610ea2565b5060fe84146110be565b9190811015610c1c5760051b81013590603e1981360301821215610145570190565b903590601e1981360301821215610145570180359067ffffffffffffffff82116101455760200191813603831361014557565b8051600091825b82811061145c5750505050565b6114668183610e8e565b5184806001600160a01b03835116602090818501516040809601519283519301915af11561149d575061149890610e7f565b61144f565b513d908186823efd5b6000906113b56114c0936001600160a01b03953691610c21565b16600052600060205260406000205415156000146114e357630b135d3f60e11b90565b6001600160e01b031990565b156114f657565b606460405162461bcd60e51b815260206004820152601c60248201527f6163636f756e743a206e6f742066726f6d20656e747279706f696e74000000006044820152fd5b805160401015610c1c5760600190565b908151811015610c1c570160200190565b906000199161156d838251018261154a565b5160f81c928151908101908111610bd55761158890826115e0565b9190565b60208151111561159c5760209052565b606460405162461bcd60e51b815260206004820152601860248201527f42797465734c69623a206f6e6c7920736872696e6b696e6700000000000000006044820152fd5b818151111561159c5752565b156115f357565b606460405162461bcd60e51b815260206004820152600960248201527f53565f5349474c454e00000000000000000000000000000000000000000000006044820152fd5b6007111561164157565b634e487b7160e01b600052602160045260246000fd5b1561165e57565b606460405162461bcd60e51b815260206004820152600a60248201527f53565f5349474d4f4445000000000000000000000000000000000000000000006044820152fd5b156116a957565b606460405162461bcd60e51b815260206004820152600f60248201527f53565f53504f4f465f4f524947494e00000000000000000000000000000000006044820152fd5b156116f457565b606460405162461bcd60e51b815260206004820152600c60248201527f53565f53504f4f465f4c454e00000000000000000000000000000000000000006044820152fd5b6040513d6000823e3d90fd5b1561014557565b908160209103126101455751610abe816101eb565b1561176757565b606460405162461bcd60e51b815260206004820152600d60248201527f53565f4c454e5f57414c4c4554000000000000000000000000000000000000006044820152fd5b908160209103126101455751610abe81610133565b604090610abe939281528160208201520190610a6d565b156117de57565b606460405162461bcd60e51b815260206004820152601160248201527f53565f57414c4c45545f494e56414c49440000000000000000000000000000006044820152fd5b1561182957565b606460405162461bcd60e51b815260206004820152600b60248201527f53565f5a45524f5f5349470000000000000000000000000000000000000000006044820152fd5b90602090818382031261014557825167ffffffffffffffff93848211610145570181601f820112156101455780516118a481610ca3565b946118b260405196876109bb565b818652848087019260051b8401019380851161014557858401925b8584106118de575050505050505090565b83518381116101455787916118f8848480948a0101610cbb565b8152019301926118cd565b9190826080910312610145578151916020810151916060604083015192015160ff811681036101455790565b1561193657565b606460405162461bcd60e51b815260206004820152601160248201527f53565f5343484e4f52525f4641494c45440000000000000000000000000000006044820152fd5b1561198157565b606460405162461bcd60e51b815260206004820152600660248201527f53565f4c454e00000000000000000000000000000000000000000000000000006044820152fd5b9092916000916119d7855115156115ec565b60ff611a146110ab6119ee6000198951018961154a565b517fff000000000000000000000000000000000000000000000000000000000000001690565b1691611a2260068410611657565b611a2b83611637565b611a3483611637565b82158015611f83575b15611b3a5750908483949592611a59604260209695511461197a565b611a6282611f96565b916001611a7d6110ab6119ee611a7785611fea565b9461153a565b94611a8781611637565b14611ae0575b611ab89192604051948594859094939260ff6060936080840197845216602083015260408201520152565b838052039060015afa15611adb5751610abe6001600160a01b0382161515611822565b611738565b611ab891604051611b3081611b228a82019485603c917f19457468657265756d205369676e6564204d6573736167653a0a3332000000008252601c8201520190565b03601f1981018352826109bb565b5190209150611a8d565b909291611b4681611637565b60048103611ce5575050611be4816020611bb493611b8388611b75611b6f611bbd9a9b51610bc6565b826115e0565b838082518301019101611903565b969092829a8199899583611bad70014551231950b75fc4402da1732fc9bebe198096819409610bda565b9609610bda565b91841415611744565b88604051948594859094939260ff6060936080840197845216602083015260408201520152565b838052039060015afa15611adb57610abe9461133e948493611c79611c96945192611c196001600160a01b0385161515611822565b6040519687936020850195869290605594927fff00000000000000000000000000000000000000000000000000000000000000916bffffffffffffffffffffffff199060601b16855260f81b166014840152601583015260358201520190565b0393611c8d601f19958681018352826109bb565b5190201461192f565b611cd660405191826112326020820195866027917f5343484e4f525200000000000000000000000000000000000000000000000000825260078201520190565b5190206001600160a01b031690565b611cf181969496611637565b60058103611daf575050611d0e611d088351610bc6565b836115e0565b611d2260209283808251830101910161186d565b93819482955b81518714611d9457611d4584611d3e8985610e8e565b51856119c5565b61133e611d8e92611b22611cd6611d88946040519283918c83019586906028926bffffffffffffffffffffffff19809260601b16835260601b1660148201520190565b96610e7f565b95611d28565b94955050505050610abe6001600160a01b0382161515611822565b611dbd819693949596611637565b60028103611e82575050611e2291611dd86021855111611760565b60206001600160a01b038119865101611e04611dfd61133e61133e61133e858c611ffa565b91886115e0565b169460405180958192630b135d3f60e11b96878452600484016117c0565b0381875afa8015611adb57611e49936001600160e01b03199291611e54575b5016146117d7565b610abe811515611822565b611e75915060203d8111611e7b575b611e6d81836109bb565b8101906117ab565b38611e41565b503d611e63565b6003919250611e9081611637565b149081611f7b575b50611ee25760405162461bcd60e51b815260206004820152600760248201527f53565f54595045000000000000000000000000000000000000000000000000006044820152606490fd5b600132148015611f70575b611ef6906116a2565b611f0360218351146116ed565b611f0c8261158c565b60208160405180611f3481906000606060808401938281528260208201528260408201520152565b838052039060015afa15611adb57610abe91611f61611b396001600160a01b0361133e9451161415611744565b6020808251830101910161174b565b5032611b3914611eed565b905038611e98565b50611f8d83611637565b60018314611a3d565b6020815110611fa6576020015190565b606460405162461bcd60e51b815260206004820152601060248201527f42797465734c69623a206c656e677468000000000000000000000000000000006044820152fd5b6040815110611fa6576040015190565b9060208101808211610bd557825110611fa65701602001519056fea26469706673582212206097aacdc07924bfc494c65c73cfe93c46d96d14d74248ecc10b767b1dd5806e64736f6c63430008130033",
709 | "deployedBytecode": "0x6080604052600436101561001e575b361561001c5761001c6108da565b005b60003560e01c806301ffc9a71461012e5780630d5828d414610129578063150b7a0214610124578063160aa0ef1461011f5780631626ba7e1461011a5780633628d042146101155780633a871cdd146101105780636171d1c91461010b5780636769de8214610106578063a2ea676614610101578063abc5345e146100fc578063affed0e0146100f7578063b0d691fe146100f2578063bc197c81146100ed578063bf1cb383146100e8578063c066a5b1146100e35763f23a6e610361000e57610867565b610829565b6107c0565b610717565b6106d3565b6106b5565b610672565b6105f2565b6105cc565b61054c565b610422565b61036d565b61031c565b6102f0565b610296565b6101fc565b61014a565b6001600160e01b031981160361014557565b600080fd5b346101455760203660031901126101455760206001600160e01b031960043561017281610133565b167f01ffc9a70000000000000000000000000000000000000000000000000000000081149081156101da575b81156101b0575b506040519015158152f35b7f4e2312e000000000000000000000000000000000000000000000000000000000915014386101a5565b630a85bd0160e11b8114915061019e565b6001600160a01b0381160361014557565b604036600319011261014557600435610214816101eb565b7f08ac40e0195c5998554e98853c23964a13a24309e127b2d016e8ec4adecf5e8360206001600160a01b036024359361024e30331461091d565b1692836000526000825280604060002055604051908152a2005b9181601f840112156101455782359167ffffffffffffffff8311610145576020838186019501011161014557565b34610145576080366003190112610145576102b26004356101eb565b6102bd6024356101eb565b60643567ffffffffffffffff8111610145576102dd903690600401610268565b50506020604051630a85bd0160e11b8152f35b346101455760203660031901126101455760043560005260026020526020604060002054604051908152f35b346101455760403660031901126101455760243567ffffffffffffffff81116101455761035b6103526020923690600401610268565b906004356114a6565b6001600160e01b031960405191168152f35b608036600319011261014557600435610385816101eb565b6024359060443567ffffffffffffffff8111610145576103a9903690600401610268565b90916103b630331461091d565b600080604051848682378085810183815203908785606435f1926103d86109f9565b93156103e057005b7f80c2637928bd94d0e1a90eaac1efc1553be6391d962fe5c82a01e90122c84ccc936001600160a01b039361041d92604051958695169785610a92565b0390a2005b3461014557600319606036820112610145576004359067ffffffffffffffff821161014557610160908236030112610145576104af6104f5916104d76104be604435926104b660009586926104a16001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001633146114ef565b610144810190600401611415565b3691610c21565b6024356119c5565b6001600160a01b03166000526000602052604060002090565b5415610512575b806104f9575b506040519081529081906020820190565b0390f35b600080808093335af15061050b6109f9565b50386104e4565b600191506104de565b9181601f840112156101455782359167ffffffffffffffff8311610145576020808501948460051b01011161014557565b60403660031901126101455767ffffffffffffffff6004358181116101455761057990369060040161051b565b906024359283116101455761059561001c933690600401610268565b929091611030565b6020600319820112610145576004359067ffffffffffffffff8211610145576105c89160040161051b565b9091565b61001c6105ed6105db3661059d565b6105e630331461091d565b3691610f24565b611448565b6105fb3661059d565b60005b81810361060757005b6106128183856113f3565b803590601e198136030182121561014557019081359167ffffffffffffffff8311610145576020809101928060051b360384136101455761066d936105956106689361065f86898b6113f3565b90810190611415565b610e7f565b6105fe565b6105ed61069c6106813661059d565b9290600093338552846020526105e660408620541515610c58565b338152806020526106b260408220541515610fe5565b80f35b34610145576000366003190112610145576020600154604051908152f35b346101455760003660031901126101455760206040516001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000168152f35b346101455760a0366003190112610145576107336004356101eb565b61073e6024356101eb565b67ffffffffffffffff6044358181116101455761075f90369060040161051b565b50506064358181116101455761077990369060040161051b565b505060843590811161014557610793903690600401610268565b50506040517fbc197c81000000000000000000000000000000000000000000000000000000008152602090f35b6060366003190112610145576004356107d8816101eb565b6024359060443567ffffffffffffffff8111610145576107fc903690600401610268565b909161080930331461091d565b6000805a604051858782378785828881018681520393f1926103d86109f9565b34610145576020366003190112610145576001600160a01b0360043561084e816101eb565b1660005260006020526020604060002054604051908152f35b346101455760a0366003190112610145576108836004356101eb565b61088e6024356101eb565b60843567ffffffffffffffff8111610145576108ae903690600401610268565b505060206040517ff23a6e61000000000000000000000000000000000000000000000000000000008152f35b6000906169698252816020526001600160a01b036040832054169081156109185750818091368280378136915af4903d918282803e1561091657f35bfd5b915050565b1561092457565b606460405162461bcd60e51b815260206004820152601660248201527f4f4e4c595f4944454e544954595f43414e5f43414c4c000000000000000000006044820152fd5b634e487b7160e01b600052604160045260246000fd5b6040810190811067ffffffffffffffff82111761099a57604052565b610968565b6060810190811067ffffffffffffffff82111761099a57604052565b90601f8019910116810190811067ffffffffffffffff82111761099a57604052565b67ffffffffffffffff811161099a57601f01601f191660200190565b3d15610a24573d90610a0a826109dd565b91610a1860405193846109bb565b82523d6000602084013e565b606090565b908060209392818452848401376000828201840152601f01601f1916010190565b60005b838110610a5d5750506000910152565b8181015183820152602001610a4d565b90602091610a8681518092818552858086019101610a4a565b601f01601f1916010190565b9291610abe9492610ab0928552606060208601526060850191610a29565b916040818403910152610a6d565b90565b90808352602080930192838260051b850194846000925b858410610ae9575050505050505090565b9091929394959681810384528735605e19843603018112156101455783016001600160a01b038135610b1a816101eb565b168252868101358783015260409081810135601e198236030181121561014557019087823592019267ffffffffffffffff83116101455782360384136101455760019389938493610b7393606080928201520191610a29565b990194019401929594939190610ad8565b90926080926001600160a01b03610abe9795168352602083015260408201528160608201520191610ac1565b634e487b7160e01b600052601160045260246000fd5b600019810191908211610bd557565b610bb0565b9070014551231950b75fc4402da1732fc9bebe19918203918211610bd557565b634e487b7160e01b600052603260045260246000fd5b90821015610c1c570190565b610bfa565b929192610c2d826109dd565b91610c3b60405193846109bb565b829481845281830111610145578281602093846000960137010152565b15610c5f57565b606460405162461bcd60e51b815260206004820152601660248201527f494e53554646494349454e545f50524956494c454745000000000000000000006044820152fd5b67ffffffffffffffff811161099a5760051b60200190565b81601f82011215610145578051610cd1816109dd565b92610cdf60405194856109bb565b8184526020828401011161014557610abe9160208085019101610a4a565b5190610d08826101eb565b565b909160608284031261014557815167ffffffffffffffff908181116101455783019360408582031261014557604051610d428161097e565b855183811161014557860182601f8201121561014557805196610d6488610ca3565b610d7160405191826109bb565b88815260209860051b83018901898201868211610145578a80809601915b838310610dc5575050505083520151868201529484015191821161014557610dbe604091610abe938601610cbb565b9301610cfd565b81908351610dd2816101eb565b8152019101908590610d8f565b6020808252825160408383015280516060840181905260808401949183019060005b818110610e175750505090604091015191015290565b82516001600160a01b031687529584019591840191600101610e01565b15610e3b57565b606460405162461bcd60e51b815260206004820152601760248201527f5245434f564552595f4e4f545f415554484f52495a45440000000000000000006044820152fd5b6000198114610bd55760010190565b8051821015610c1c5760209160051b010190565b9060018201809211610bd557565b91908201809211610bd557565b610abe9492606092825260208201528160408201520191610ac1565b15610ee057565b606460405162461bcd60e51b815260206004820152601260248201527f5245434f564552595f4e4f545f524541445900000000000000000000000000006044820152fd5b929190610f3081610ca3565b91604091610f40835194856109bb565b839581855260208095019160051b8101938385116101455781925b858410610f6b5750505050505050565b67ffffffffffffffff9084358281116101455784019160608388031261014557835192610f978461099f565b8035610fa2816101eb565b8452898101358a8501528481013591821161014557019086601f83011215610145578892610fd68884868096359101610c21565b85820152815201930192610f5b565b15610fec57565b606460405162461bcd60e51b815260206004820152601860248201527f50524956494c4547455f4e4f545f444f574e47524144454400000000000000006044820152fd5b9192600190815491604091825190602080830183611052898b8a463087610b84565b0393611066601f19958681018352826109bb565b5190209860ff6110b16110ab61108561107e87610bc6565b8786610c10565b357fff000000000000000000000000000000000000000000000000000000000000001690565b60f81c90565b169260ff841480156113e9575b1561138757916110f46110e56110e060fe9d9896946111489a98963691610c21565b61155b565b50828082518301019101610d0a565b939061114f6001600160a01b038096169e8f9314928951868101906111298161111d8985610ddf565b038b81018352826109bb565b5190209b8c916001600160a01b03166000526000602052604060002090565b5414610e34565b611163896000526002602052604060002090565b54958615158061137f575b156111f8575050505050506104be956105ed956111f099957fdef2c5a081ab1dd3f896874500de5f4045efd102a452bce3af5123459f461f1b610d089c9a966111d06111cb6105e6986111c56111cb994211610ed9565b54610e7f565b600155565b6000838152600260205260408120555142815280602081010390a3610ea2565b541515610fe5565b61124c93959e50879d999b989a9c97929496508b9085600014611373578b518881019283526363616e6360208401529061123e9082604085015b039081018352826109bb565b51902090829e9392916119c5565b6000918291169b5b611314575b5061126e9192939495969798999a9b50610e34565b156112c05750505050507ff38fc5203ec5ac432b66df0ed37402f7b1afb377ddce98e08c2bf696c71874059060006112b0846000526002602052604060002090565b555142815280602081015b0390a4565b946112f36112bb93927f58d616db4bbaccaaad10b948d290b9df7973a7b566f83a68e2e0e7f4e5c61ccd97015142610eb0565b611307886000526002602052604060002090565b5551938493429085610ebd565b8351805182101561136d5761134a61133e6113318f938590610e8e565b516001600160a01b031690565b6001600160a01b031690565b1461135f576113598d91610e7f565b90611254565b50999a8b9a5061126e611259565b50611259565b5090829e9392916119c5565b50821561116e565b6105ed96506104be979894506105e693506111cb925094610d089a6113b56113bb936111f09c983691610c21565b906119c5565b966113e46113dc896001600160a01b03166000526000602052604060002090565b541515610c58565b610ea2565b5060fe84146110be565b9190811015610c1c5760051b81013590603e1981360301821215610145570190565b903590601e1981360301821215610145570180359067ffffffffffffffff82116101455760200191813603831361014557565b8051600091825b82811061145c5750505050565b6114668183610e8e565b5184806001600160a01b03835116602090818501516040809601519283519301915af11561149d575061149890610e7f565b61144f565b513d908186823efd5b6000906113b56114c0936001600160a01b03953691610c21565b16600052600060205260406000205415156000146114e357630b135d3f60e11b90565b6001600160e01b031990565b156114f657565b606460405162461bcd60e51b815260206004820152601c60248201527f6163636f756e743a206e6f742066726f6d20656e747279706f696e74000000006044820152fd5b805160401015610c1c5760600190565b908151811015610c1c570160200190565b906000199161156d838251018261154a565b5160f81c928151908101908111610bd55761158890826115e0565b9190565b60208151111561159c5760209052565b606460405162461bcd60e51b815260206004820152601860248201527f42797465734c69623a206f6e6c7920736872696e6b696e6700000000000000006044820152fd5b818151111561159c5752565b156115f357565b606460405162461bcd60e51b815260206004820152600960248201527f53565f5349474c454e00000000000000000000000000000000000000000000006044820152fd5b6007111561164157565b634e487b7160e01b600052602160045260246000fd5b1561165e57565b606460405162461bcd60e51b815260206004820152600a60248201527f53565f5349474d4f4445000000000000000000000000000000000000000000006044820152fd5b156116a957565b606460405162461bcd60e51b815260206004820152600f60248201527f53565f53504f4f465f4f524947494e00000000000000000000000000000000006044820152fd5b156116f457565b606460405162461bcd60e51b815260206004820152600c60248201527f53565f53504f4f465f4c454e00000000000000000000000000000000000000006044820152fd5b6040513d6000823e3d90fd5b1561014557565b908160209103126101455751610abe816101eb565b1561176757565b606460405162461bcd60e51b815260206004820152600d60248201527f53565f4c454e5f57414c4c4554000000000000000000000000000000000000006044820152fd5b908160209103126101455751610abe81610133565b604090610abe939281528160208201520190610a6d565b156117de57565b606460405162461bcd60e51b815260206004820152601160248201527f53565f57414c4c45545f494e56414c49440000000000000000000000000000006044820152fd5b1561182957565b606460405162461bcd60e51b815260206004820152600b60248201527f53565f5a45524f5f5349470000000000000000000000000000000000000000006044820152fd5b90602090818382031261014557825167ffffffffffffffff93848211610145570181601f820112156101455780516118a481610ca3565b946118b260405196876109bb565b818652848087019260051b8401019380851161014557858401925b8584106118de575050505050505090565b83518381116101455787916118f8848480948a0101610cbb565b8152019301926118cd565b9190826080910312610145578151916020810151916060604083015192015160ff811681036101455790565b1561193657565b606460405162461bcd60e51b815260206004820152601160248201527f53565f5343484e4f52525f4641494c45440000000000000000000000000000006044820152fd5b1561198157565b606460405162461bcd60e51b815260206004820152600660248201527f53565f4c454e00000000000000000000000000000000000000000000000000006044820152fd5b9092916000916119d7855115156115ec565b60ff611a146110ab6119ee6000198951018961154a565b517fff000000000000000000000000000000000000000000000000000000000000001690565b1691611a2260068410611657565b611a2b83611637565b611a3483611637565b82158015611f83575b15611b3a5750908483949592611a59604260209695511461197a565b611a6282611f96565b916001611a7d6110ab6119ee611a7785611fea565b9461153a565b94611a8781611637565b14611ae0575b611ab89192604051948594859094939260ff6060936080840197845216602083015260408201520152565b838052039060015afa15611adb5751610abe6001600160a01b0382161515611822565b611738565b611ab891604051611b3081611b228a82019485603c917f19457468657265756d205369676e6564204d6573736167653a0a3332000000008252601c8201520190565b03601f1981018352826109bb565b5190209150611a8d565b909291611b4681611637565b60048103611ce5575050611be4816020611bb493611b8388611b75611b6f611bbd9a9b51610bc6565b826115e0565b838082518301019101611903565b969092829a8199899583611bad70014551231950b75fc4402da1732fc9bebe198096819409610bda565b9609610bda565b91841415611744565b88604051948594859094939260ff6060936080840197845216602083015260408201520152565b838052039060015afa15611adb57610abe9461133e948493611c79611c96945192611c196001600160a01b0385161515611822565b6040519687936020850195869290605594927fff00000000000000000000000000000000000000000000000000000000000000916bffffffffffffffffffffffff199060601b16855260f81b166014840152601583015260358201520190565b0393611c8d601f19958681018352826109bb565b5190201461192f565b611cd660405191826112326020820195866027917f5343484e4f525200000000000000000000000000000000000000000000000000825260078201520190565b5190206001600160a01b031690565b611cf181969496611637565b60058103611daf575050611d0e611d088351610bc6565b836115e0565b611d2260209283808251830101910161186d565b93819482955b81518714611d9457611d4584611d3e8985610e8e565b51856119c5565b61133e611d8e92611b22611cd6611d88946040519283918c83019586906028926bffffffffffffffffffffffff19809260601b16835260601b1660148201520190565b96610e7f565b95611d28565b94955050505050610abe6001600160a01b0382161515611822565b611dbd819693949596611637565b60028103611e82575050611e2291611dd86021855111611760565b60206001600160a01b038119865101611e04611dfd61133e61133e61133e858c611ffa565b91886115e0565b169460405180958192630b135d3f60e11b96878452600484016117c0565b0381875afa8015611adb57611e49936001600160e01b03199291611e54575b5016146117d7565b610abe811515611822565b611e75915060203d8111611e7b575b611e6d81836109bb565b8101906117ab565b38611e41565b503d611e63565b6003919250611e9081611637565b149081611f7b575b50611ee25760405162461bcd60e51b815260206004820152600760248201527f53565f54595045000000000000000000000000000000000000000000000000006044820152606490fd5b600132148015611f70575b611ef6906116a2565b611f0360218351146116ed565b611f0c8261158c565b60208160405180611f3481906000606060808401938281528260208201528260408201520152565b838052039060015afa15611adb57610abe91611f61611b396001600160a01b0361133e9451161415611744565b6020808251830101910161174b565b5032611b3914611eed565b905038611e98565b50611f8d83611637565b60018314611a3d565b6020815110611fa6576020015190565b606460405162461bcd60e51b815260206004820152601060248201527f42797465734c69623a206c656e677468000000000000000000000000000000006044820152fd5b6040815110611fa6576040015190565b9060208101808211610bd557825110611fa65701602001519056fea26469706673582212206097aacdc07924bfc494c65c73cfe93c46d96d14d74248ecc10b767b1dd5806e64736f6c63430008130033",
710 | "linkReferences": {},
711 | "deployedLinkReferences": {}
712 | }
713 |
--------------------------------------------------------------------------------