├── 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 | ![ETH Prague logo](https://pbs.twimg.com/profile_images/1633826299495034880/zXuo99FD_400x400.jpg) 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 | qr seal logo 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 |
161 | 162 | To: 163 | 172 | 173 | 174 | Amount: 175 | 186 | 187 | 195 |
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 |
262 | 263 | To: 264 | 273 | 274 | 275 | Amount: 276 | 287 | 288 | 289 | 290 | 291 | 299 |
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 | --------------------------------------------------------------------------------