├── 01-defender-meta-txs
├── .gitignore
├── README.md
├── app
│ ├── .env
│ ├── .gitignore
│ ├── README.md
│ ├── package.json
│ ├── public
│ │ ├── favicon.ico
│ │ ├── index.html
│ │ ├── logo192.png
│ │ ├── logo512.png
│ │ ├── manifest.json
│ │ └── robots.txt
│ ├── src
│ │ ├── components
│ │ │ ├── App.css
│ │ │ ├── App.js
│ │ │ ├── Register.css
│ │ │ ├── Register.js
│ │ │ ├── Registrations.css
│ │ │ └── Registrations.js
│ │ ├── deploy.json
│ │ ├── eth
│ │ │ ├── context.js
│ │ │ ├── forwarder.js
│ │ │ ├── provider.js
│ │ │ ├── register.js
│ │ │ ├── registry.js
│ │ │ └── signer.js
│ │ ├── index.css
│ │ └── index.js
│ └── yarn.lock
├── autotasks
│ └── relay
│ │ └── index.js
├── contracts
│ ├── Registry.sol
│ └── SimpleRegistry.sol
├── deploy.json
├── hardhat.config.js
├── package.json
├── rollup.config.js
├── scripts
│ ├── deploy.js
│ ├── events.js
│ ├── invoke.sh
│ ├── relay.js
│ ├── sign.js
│ ├── upload.js
│ └── verify.js
├── slides
│ └── 20210211 - Defender meta-txs workshop.pdf
├── src
│ ├── forwarder.js
│ └── signer.js
├── test
│ ├── contracts
│ │ ├── Registry.test.js
│ │ └── SimpleRegistry.test.js
│ └── src
│ │ └── relay.test.js
└── yarn.lock
├── 02-contracts-clone
├── .gitignore
├── README.md
├── contracts
│ ├── 1-ERC20
│ │ ├── FactoryClone.sol
│ │ ├── FactoryNaive.sol
│ │ └── FactoryProxy.sol
│ ├── 2-uniswap
│ │ ├── UniswapV2FactoryClones.sol
│ │ ├── UniswapV2PairClones.sol
│ │ └── import.sol
│ └── 3-argent
│ │ ├── WalletFactoryClones.sol
│ │ ├── import_0.5.sol
│ │ └── import_0.6.sol
├── hardhat.config.js
├── package-lock.json
├── package.json
├── slides
│ └── 20210227 - Contracts clones workshop.pdf
├── test
│ ├── 1-ERC20.test.js
│ ├── 2-Uniswap.test.js
│ └── 3-Argent.test.js
└── yarn.lock
├── 03-defender-service-monitoring
├── README.md
├── code
│ ├── greeting-example
│ │ ├── Greeter.sol
│ │ └── autotask-pause-greeter.js
│ └── yearn-example
│ │ └── autotask-loss-detector.js
└── slides
│ └── 20210311 - Service Monitoring and Emergency Response Workshop.pdf
├── 04-roles
└── slides.pdf
├── 05-upgrades-management
├── code
│ ├── .gitignore
│ ├── README.md
│ ├── contracts
│ │ ├── Box.sol
│ │ ├── BoxV2.sol
│ │ ├── BoxV3.sol
│ │ └── Greeter.sol
│ ├── hardhat.config.js
│ ├── package-lock.json
│ ├── package.json
│ ├── scripts
│ │ ├── deploy.js
│ │ ├── propose-upgrade.js
│ │ ├── sample-script.js
│ │ ├── transfer-ownership.js
│ │ └── upgrade.js
│ └── test
│ │ └── sample-test.js
└── slides
│ └── Upgrades workshop.pdf
├── 06-nft-merkle-drop
├── .gitignore
├── contracts
│ ├── ERC721Basic.sol
│ ├── ERC721LazyMint.sol
│ ├── ERC721LazyMintWith712.sol
│ ├── ERC721LazyMintWith712SignatureChecker.sol
│ ├── ERC721MerkleDrop.sol
│ └── SmartWallet.sol
├── hardhat.config.js
├── package-lock.json
├── package.json
├── scripts
│ ├── 1-deploy.js
│ ├── 2-sign.js
│ ├── 3-redeem.js
│ └── commands.sh
├── slides
│ └── 20210506 - Lazy minting workshop.pdf
├── test
│ ├── 0-ERC721Mint.test.js
│ ├── 1-ERC721LazyMint.test.js
│ ├── 2-ERC721LazyMintWith712.test.js
│ ├── 3-ERC721LazyMintWith712Signature.test.js
│ ├── 4-ERC721MerkleDrop.test.js
│ └── tokens.json
└── yarn.lock
├── 07-automate-workflows
├── .gitignore
├── abi
│ └── NFT.json
├── bin
│ └── local.js
├── package.json
├── slides
│ └── Automating Smart Contract Workflows.pdf
├── src
│ └── index.js
└── yarn.lock
├── 08-uups-proxies
├── .gitignore
├── contracts
│ └── Mars.sol
├── hardhat.config.ts
├── package-lock.json
├── package.json
├── slides.pdf
├── test
│ └── Mars.test.ts
└── tsconfig.json
├── 09-chainlink-defender
└── slides.pdf
├── 10-pooltogether-defender
└── Slides.pdf
├── 11-dangers-token-integration
└── slides.pdf
├── 12-subgraphs
├── .gitignore
├── package-lock.json
├── package.json
├── sample.json
├── slides.pdf
├── subgraph.ens.yaml
├── subgraph.poap.yaml
└── subgraph.stonercats.yaml
├── 13-secure-development-and-ops
├── .gitignore
├── .openzeppelin
│ └── unknown-1287.json
├── autotasks
│ └── mint.js
├── contracts
│ └── Token.sol
├── hardhat.config.js
├── package.json
├── scripts
│ ├── deploy.js
│ ├── get-version.js
│ └── upgrade.js
├── sentinels
│ └── transfers.txt
├── slides.pdf
└── yarn.lock
├── 14-strategies-secure-access-controls
└── slides.pdf
├── 15-timelock-defender
└── slides.pdf
├── 16-dangers-price-oracles-smart-contracts
└── slides.pdf
├── 17-on-chain-governance
└── slides.pdf
├── 18-strategies-secure-governance
└── slides.pdf
├── 19-security-upgrades-smart-contracts
└── slides.pdf
├── 20-community-call-1
└── 2021-11-08 - Community Call #1.pdf
├── 21-ens-governor
└── slides.pdf
├── 22-onward-smart-contract-security
└── slides.pdf
├── 23-community-call-2
└── 2022-01-18 - Community Call #2.pdf
├── 24-defender-relayer-civtrade-order-book
└── Defender - CivTrade Workshop.pdf
├── 25-defender-metatx-api
├── .DS_Store
├── .env.example
├── .gitignore
├── README.md
├── action
│ └── index.mjs
├── app
│ ├── .gitignore
│ ├── README.md
│ ├── config-overrides.js
│ ├── package.json
│ ├── public
│ │ ├── favicon.ico
│ │ ├── index.html
│ │ ├── logo192.png
│ │ ├── logo512.png
│ │ ├── manifest.json
│ │ └── robots.txt
│ ├── src
│ │ ├── components
│ │ │ ├── App.css
│ │ │ ├── App.js
│ │ │ ├── Register.css
│ │ │ ├── Register.js
│ │ │ ├── Registrations.css
│ │ │ └── Registrations.js
│ │ ├── deploy.json
│ │ ├── eth
│ │ │ ├── context.js
│ │ │ ├── forwarder.js
│ │ │ ├── provider.js
│ │ │ ├── register.js
│ │ │ ├── registry.js
│ │ │ └── signer.js
│ │ ├── index.css
│ │ └── index.js
│ └── yarn.lock
├── contracts
│ ├── Registry.sol
│ └── SimpleRegistry.sol
├── deploy.json
├── hardhat.config.js
├── package-lock.json
├── package.json
├── rollup.config.mjs
├── scripts
│ ├── createAction.js
│ ├── createRelayer.js
│ ├── deploy.js
│ ├── events.js
│ ├── invoke.sh
│ ├── relay.js
│ ├── sign.js
│ ├── upload.js
│ └── verify.js
├── src
│ ├── forwarder.js
│ └── signer.js
├── test
│ ├── contracts
│ │ ├── Registry.test.js
│ │ └── SimpleRegistry.test.js
│ └── src
│ │ └── relay.test.js
└── yarn.lock
├── 26-nft-monitoring-defender
├── .env.example
├── .eslintignore
├── .eslintrc.js
├── .gitignore
├── .prettierignore
├── .solhint.json
├── .solhintignore
├── README.md
├── contracts
│ └── NFT.sol
├── defender
│ ├── autotask-1
│ │ └── index.js
│ └── serverless.yml
├── hardhat.config.ts
├── package-lock.json
├── package.json
├── scripts
│ └── deploy.ts
├── secrets-example.yml
├── tsconfig.json
└── yarn.lock
└── README.md
/01-defender-meta-txs/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | artifacts
3 | cache
4 | deploy.*.json
5 | /.env
6 | tmp
7 | build
8 |
--------------------------------------------------------------------------------
/01-defender-meta-txs/app/.env:
--------------------------------------------------------------------------------
1 | REACT_APP_WEBHOOK_URL=
2 | REACT_APP_QUICKNODE_URL=
3 |
--------------------------------------------------------------------------------
/01-defender-meta-txs/app/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/01-defender-meta-txs/app/README.md:
--------------------------------------------------------------------------------
1 | # Meta-transactions sample client app
2 |
3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
4 |
5 | ## Available Scripts
6 |
7 | In the project directory, you can run:
8 |
9 | ### `yarn start`
10 |
11 | Runs the app in the development mode.\
12 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
13 |
14 | The page will reload if you make edits.\
15 | You will also see any lint errors in the console.
16 |
17 |
18 | ### `yarn build`
19 |
20 | Builds the app for production to the `build` folder.\
21 | It correctly bundles React in production mode and optimizes the build for the best performance.
22 |
23 | The build is minified and the filenames include the hashes.\
24 | Your app is ready to be deployed!
25 |
26 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
27 |
28 |
--------------------------------------------------------------------------------
/01-defender-meta-txs/app/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "client",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@testing-library/jest-dom": "^5.11.4",
7 | "@testing-library/react": "^11.1.0",
8 | "@testing-library/user-event": "^12.1.10",
9 | "eth-sig-util": "^3.0.1",
10 | "ethers": "^5.0.30",
11 | "lodash": "^4.17.20",
12 | "react": "^17.0.1",
13 | "react-dom": "^17.0.1",
14 | "react-scripts": "4.0.2",
15 | "react-toastify": "^7.0.3",
16 | "web-vitals": "^1.0.1"
17 | },
18 | "scripts": {
19 | "start": "react-scripts start",
20 | "build": "react-scripts build",
21 | "test": "react-scripts test",
22 | "eject": "react-scripts eject"
23 | },
24 | "eslintConfig": {
25 | "extends": [
26 | "react-app",
27 | "react-app/jest"
28 | ]
29 | },
30 | "browserslist": {
31 | "production": [
32 | ">0.2%",
33 | "not dead",
34 | "not op_mini all"
35 | ],
36 | "development": [
37 | "last 1 chrome version",
38 | "last 1 firefox version",
39 | "last 1 safari version"
40 | ]
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/01-defender-meta-txs/app/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OpenZeppelin/workshops/ca3ef410f805d044aa6ac2b95be16104baf90fbf/01-defender-meta-txs/app/public/favicon.ico
--------------------------------------------------------------------------------
/01-defender-meta-txs/app/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | Names Registry
28 |
29 |
30 |
31 |
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/01-defender-meta-txs/app/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OpenZeppelin/workshops/ca3ef410f805d044aa6ac2b95be16104baf90fbf/01-defender-meta-txs/app/public/logo192.png
--------------------------------------------------------------------------------
/01-defender-meta-txs/app/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OpenZeppelin/workshops/ca3ef410f805d044aa6ac2b95be16104baf90fbf/01-defender-meta-txs/app/public/logo512.png
--------------------------------------------------------------------------------
/01-defender-meta-txs/app/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/01-defender-meta-txs/app/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/01-defender-meta-txs/app/src/components/App.css:
--------------------------------------------------------------------------------
1 | @import url('https://fonts.googleapis.com/css2?family=Nunito+Sans:wght@900&display=swap');
2 |
3 | body {
4 | background: #F9FAFB;
5 | }
6 |
7 | .App {
8 | text-align: center;
9 | }
10 |
11 | .App-header {
12 | background-color: #60b2e8;
13 | background-image: linear-gradient(135deg, #60b2e8 0%, #9599E2 100%);
14 | padding: 6rem 0 8rem 0;
15 | display: flex;
16 | flex-direction: column;
17 | align-items: center;
18 | justify-content: center;
19 | font-size: 1.5rem;
20 | color: white;
21 | }
22 |
23 | .App-header h1 {
24 | margin: 0;
25 | font-family: 'Nunito Sans', sans-serif;
26 | }
27 |
28 | .App-header p {
29 | margin-top: .5rem;
30 | font-size: 1.25rem;
31 | }
32 |
33 | .App-link {
34 | color: #61dafb;
35 | }
36 |
37 | .App-content {
38 | max-width: 60rem;
39 | margin: auto;
40 | text-align: left;
41 | }
--------------------------------------------------------------------------------
/01-defender-meta-txs/app/src/components/App.js:
--------------------------------------------------------------------------------
1 | import { EthereumContext } from '../eth/context';
2 | import { createProvider } from '../eth/provider';
3 | import { createInstance } from '../eth/registry';
4 |
5 | import './App.css';
6 | import Registrations from './Registrations';
7 | import Register from './Register';
8 |
9 | import { ToastContainer } from 'react-toastify';
10 | import 'react-toastify/dist/ReactToastify.css';
11 |
12 | function App() {
13 | const provider = createProvider();
14 | const registry = createInstance(provider);
15 | const ethereumContext = { provider, registry };
16 |
17 | return (
18 |
31 | );
32 | }
33 |
34 | export default App;
35 |
--------------------------------------------------------------------------------
/01-defender-meta-txs/app/src/components/Register.css:
--------------------------------------------------------------------------------
1 | form {
2 | display: flex;
3 | justify-content: center;
4 | align-items: center;
5 | }
6 |
7 | input {
8 | flex-grow: 6;
9 | border: 1px solid #D8DBE2;
10 | box-sizing: border-box;
11 | border-radius: 6px;
12 | padding: 1rem 1.5rem;
13 | font-size: 1rem;
14 | }
15 |
16 | button {
17 | width: 9rem;
18 | background: #4E5DE4;
19 | color: white;
20 | font-weight: 500;
21 | font-family: 'Nunito Sans', sans-serif;
22 | text-transform: uppercase;
23 | border: solid 1px #4E5DE4;
24 | border-radius: .25rem;
25 | padding: 1rem 1.5rem;
26 | margin-left: .5rem;
27 | }
28 |
29 | button:hover {
30 | background: #3442c4;
31 | cursor: pointer;
32 | }
33 |
34 | .Container {
35 | background: white;
36 | border-radius: 1rem;
37 | padding: 3rem;
38 | box-shadow: 0px 8px 16px rgba(44, 55, 75, 0.16);
39 | z-index: 2;
40 | margin: 0 2rem;
41 | margin-top: -4rem;
42 | }
43 |
44 | h4 {
45 | margin: 0;
46 | text-align: center;
47 | }
--------------------------------------------------------------------------------
/01-defender-meta-txs/app/src/components/Register.js:
--------------------------------------------------------------------------------
1 | import { useRef, useState, useContext } from 'react';
2 | import { registerName } from '../eth/register';
3 | import { EthereumContext } from "../eth/context";
4 | import { toast } from 'react-toastify';
5 | import './Register.css';
6 |
7 | function Register() {
8 | const nameInput = useRef(null);
9 | const [submitting, setSubmitting] = useState(false);
10 | const { registry, provider } = useContext(EthereumContext);
11 |
12 | const sendTx = async (event) => {
13 | event.preventDefault();
14 | const name = nameInput.current.value;
15 | setSubmitting(true);
16 |
17 | try {
18 | const response = await registerName(registry, provider, name);
19 | const hash = response.hash;
20 | const onClick = hash
21 | ? () => window.open(`https://blockscout.com/poa/xdai/tx/${hash}`)
22 | : undefined;
23 | toast('Transaction sent!', { type: 'info', onClick });
24 | nameInput.current.value = '';
25 | } catch(err) {
26 | toast(err.message || err, { type: 'error' });
27 | } finally {
28 | setSubmitting(false);
29 | }
30 | }
31 |
32 | return
33 |
37 |
38 | }
39 |
40 | export default Register;
--------------------------------------------------------------------------------
/01-defender-meta-txs/app/src/components/Registrations.css:
--------------------------------------------------------------------------------
1 | ul {
2 | list-style-type: none;
3 | padding-inline-start: 0;
4 | max-width: fit-content;
5 | margin: 1rem auto 3rem;
6 | }
7 |
8 | li {
9 | color: #2C374B;
10 | }
11 |
12 | .address {
13 | font-family: 'Courier New', Courier, monospace;
14 | text-align: left;
15 | margin-right: 1vmin;
16 | color: #575C66;
17 | }
18 |
19 | .Registrations h3 {
20 | text-align: center;
21 | margin: 3rem 0 0;
22 | color: #575C66;
23 | }
24 |
25 | .Registrations {
26 | padding: 0 2rem;
27 | }
--------------------------------------------------------------------------------
/01-defender-meta-txs/app/src/components/Registrations.js:
--------------------------------------------------------------------------------
1 | import { useContext, useState, useEffect } from "react";
2 | import { EthereumContext } from "../eth/context";
3 | import './Registrations.css';
4 |
5 | const mapEvent = (event) => ({
6 | blockNumber: event.blockNumber,
7 | who: event.args.who,
8 | name: event.args.name,
9 | id: `${event.blockHash}/${event.transactionIndex}/${event.logIndex}`,
10 | })
11 |
12 | function Registrations() {
13 | const { registry } = useContext(EthereumContext);
14 | const [registrations, setRegistrations] = useState(undefined);
15 |
16 | useEffect(() => {
17 | const filter = registry.filters.Registered();
18 |
19 | const listener = (...args) => {
20 | const event = args[args.length-1];
21 | setRegistrations(rs => [mapEvent(event), ...rs || []]);
22 | };
23 |
24 | const subscribe = async() => {
25 | const past = await registry.queryFilter(filter);
26 | setRegistrations((past.reverse() || []).map(mapEvent));
27 | registry.on(filter, listener);
28 | }
29 |
30 | subscribe();
31 | return () => registry.off(filter, listener);
32 | }, [registry]);
33 |
34 | return
35 |
Last registrations 📝
36 | {registrations === undefined && (
37 |
Loading..
38 | )}
39 | {registrations && (
40 |
41 | {registrations.map(r => (
42 | - {r.who} {r.name}
43 | ))}
44 |
45 | )}
46 |
47 | }
48 |
49 | export default Registrations;
--------------------------------------------------------------------------------
/01-defender-meta-txs/app/src/deploy.json:
--------------------------------------------------------------------------------
1 | ../../deploy.json
--------------------------------------------------------------------------------
/01-defender-meta-txs/app/src/eth/context.js:
--------------------------------------------------------------------------------
1 | import { createContext } from 'react';
2 |
3 | export const EthereumContext = createContext({ });
4 |
--------------------------------------------------------------------------------
/01-defender-meta-txs/app/src/eth/forwarder.js:
--------------------------------------------------------------------------------
1 | import { ethers } from 'ethers';
2 | import { MinimalForwarder as address } from '../deploy.json';
3 | const abi = [{"inputs":[],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[{"components":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"},{"internalType":"uint256","name":"gas","type":"uint256"},{"internalType":"uint256","name":"nonce","type":"uint256"},{"internalType":"bytes","name":"data","type":"bytes"}],"internalType":"struct MinimalForwarder.ForwardRequest","name":"req","type":"tuple"},{"internalType":"bytes","name":"signature","type":"bytes"}],"name":"execute","outputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"bytes","name":"","type":"bytes"}],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"}],"name":"getNonce","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"components":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"},{"internalType":"uint256","name":"gas","type":"uint256"},{"internalType":"uint256","name":"nonce","type":"uint256"},{"internalType":"bytes","name":"data","type":"bytes"}],"internalType":"struct MinimalForwarder.ForwardRequest","name":"req","type":"tuple"},{"internalType":"bytes","name":"signature","type":"bytes"}],"name":"verify","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"}];
4 |
5 | export function createInstance(provider) {
6 | return new ethers.Contract(address, abi, provider);
7 | }
8 |
--------------------------------------------------------------------------------
/01-defender-meta-txs/app/src/eth/provider.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-unused-vars */
2 | import { ethers } from 'ethers';
3 |
4 | const CLOUDFLARE_ENDPOINT = 'https://dai.poa.network';
5 | const MAIN_ENDPOINT = 'https://rpc.xdaichain.com/';
6 | const ALTERNATE_ENDPOINT = 'https://xdai.poanetwork.dev';
7 | const UNSECURE_ENDPOINT = 'http://xdai.poanetwork.dev';
8 | const QUICKNODE_ENDPOINT = process.env.REACT_APP_QUICKNODE_URL;
9 |
10 | export function createProvider() {
11 | return new ethers.providers.JsonRpcProvider(QUICKNODE_ENDPOINT || MAIN_ENDPOINT, 100);
12 | }
--------------------------------------------------------------------------------
/01-defender-meta-txs/app/src/eth/register.js:
--------------------------------------------------------------------------------
1 | import { ethers } from 'ethers';
2 | import { createInstance } from './forwarder';
3 | import { signMetaTxRequest } from './signer';
4 |
5 | async function sendTx(registry, name) {
6 | console.log(`Sending register tx to set name=${name}`);
7 | return registry.register(name);
8 | }
9 |
10 | async function sendMetaTx(registry, provider, signer, name) {
11 | console.log(`Sending register meta-tx to set name=${name}`);
12 | const url = process.env.REACT_APP_WEBHOOK_URL;
13 | if (!url) throw new Error(`Missing relayer url`);
14 |
15 | const forwarder = createInstance(provider);
16 | const from = await signer.getAddress();
17 | const data = registry.interface.encodeFunctionData('register', [name]);
18 | const to = registry.address;
19 |
20 | const request = await signMetaTxRequest(signer.provider, forwarder, { to, from, data });
21 |
22 | return fetch(url, {
23 | method: 'POST',
24 | body: JSON.stringify(request),
25 | headers: { 'Content-Type': 'application/json' },
26 | });
27 | }
28 |
29 | export async function registerName(registry, provider, name) {
30 | if (!name) throw new Error(`Name cannot be empty`);
31 | if (!window.ethereum) throw new Error(`User wallet not found`);
32 |
33 | await window.ethereum.enable();
34 | const userProvider = new ethers.providers.Web3Provider(window.ethereum);
35 | const userNetwork = await userProvider.getNetwork();
36 | if (userNetwork.chainId !== 100) throw new Error(`Please switch to xDAI for signing`);
37 |
38 | const signer = userProvider.getSigner();
39 | const from = await signer.getAddress();
40 | const balance = await provider.getBalance(from);
41 |
42 | const canSendTx = balance.gt(1e15);
43 | if (canSendTx) return sendTx(registry.connect(signer), name);
44 | else return sendMetaTx(registry, provider, signer, name);
45 | }
46 |
--------------------------------------------------------------------------------
/01-defender-meta-txs/app/src/eth/registry.js:
--------------------------------------------------------------------------------
1 | import { ethers } from 'ethers';
2 | import { Registry as address } from '../deploy.json';
3 |
4 | const abi = [{"inputs":[{"internalType":"contract MinimalForwarder","name":"forwarder","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"who","type":"address"},{"indexed":false,"internalType":"string","name":"name","type":"string"}],"name":"Registered","type":"event"},{"inputs":[{"internalType":"address","name":"forwarder","type":"address"}],"name":"isTrustedForwarder","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"names","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"}],"name":"owners","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"string","name":"name","type":"string"}],"name":"register","outputs":[],"stateMutability":"nonpayable","type":"function"}];
5 |
6 | export function createInstance(provider) {
7 | return new ethers.Contract(address, abi, provider);
8 | }
9 |
--------------------------------------------------------------------------------
/01-defender-meta-txs/app/src/eth/signer.js:
--------------------------------------------------------------------------------
1 | ../../../src/signer.js
--------------------------------------------------------------------------------
/01-defender-meta-txs/app/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 |
15 | h1 {
16 | font-size: 2em;
17 | margin-block-start: 1.1em;
18 | }
--------------------------------------------------------------------------------
/01-defender-meta-txs/app/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import './index.css';
4 | import App from './components/App';
5 |
6 | ReactDOM.render(
7 |
8 |
9 | ,
10 | document.getElementById('root')
11 | );
12 |
13 |
--------------------------------------------------------------------------------
/01-defender-meta-txs/autotasks/relay/index.js:
--------------------------------------------------------------------------------
1 | const ethers = require('ethers');
2 | const { DefenderRelaySigner, DefenderRelayProvider } = require('defender-relay-client/lib/ethers');
3 |
4 | const { ForwarderAbi } = require('../../src/forwarder');
5 | const ForwarderAddress = require('../../deploy.json').MinimalForwarder;
6 | const RegistryAddress = require('../../deploy.json').Registry;
7 |
8 | async function relay(forwarder, request, signature, whitelist) {
9 | // Decide if we want to relay this request based on a whitelist
10 | const accepts = !whitelist || whitelist.includes(request.to);
11 | if (!accepts) throw new Error(`Rejected request to ${request.to}`);
12 |
13 | // Validate request on the forwarder contract
14 | const valid = await forwarder.verify(request, signature);
15 | if (!valid) throw new Error(`Invalid request`);
16 |
17 | // Send meta-tx through relayer to the forwarder contract
18 | const gasLimit = (parseInt(request.gas) + 50000).toString();
19 | return await forwarder.execute(request, signature, { gasLimit });
20 | }
21 |
22 | async function handler(event) {
23 | // Parse webhook payload
24 | if (!event.request || !event.request.body) throw new Error(`Missing payload`);
25 | const { request, signature } = event.request.body;
26 | console.log(`Relaying`, request);
27 |
28 | // Initialize Relayer provider and signer, and forwarder contract
29 | const credentials = { ... event };
30 | const provider = new DefenderRelayProvider(credentials);
31 | const signer = new DefenderRelaySigner(credentials, provider, { speed: 'fast' });
32 | const forwarder = new ethers.Contract(ForwarderAddress, ForwarderAbi, signer);
33 |
34 | // Relay transaction!
35 | const tx = await relay(forwarder, request, signature);
36 | console.log(`Sent meta-tx: ${tx.hash}`);
37 | return { txHash: tx.hash };
38 | }
39 |
40 | module.exports = {
41 | handler,
42 | relay,
43 | }
--------------------------------------------------------------------------------
/01-defender-meta-txs/contracts/Registry.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity ^0.8.0;
3 |
4 | import "@openzeppelin/contracts/metatx/ERC2771Context.sol";
5 | import "@openzeppelin/contracts/metatx/MinimalForwarder.sol";
6 |
7 | contract Registry is ERC2771Context {
8 | event Registered(address indexed who, string name);
9 |
10 | mapping(address => string) public names;
11 | mapping(string => address) public owners;
12 |
13 | constructor(MinimalForwarder forwarder) // Initialize trusted forwarder
14 | ERC2771Context(address(forwarder)) {
15 | }
16 |
17 | function register(string memory name) external {
18 | require(owners[name] == address(0), "Name taken");
19 | address owner = _msgSender(); // Changed from msg.sender
20 | owners[name] = owner;
21 | names[owner] = name;
22 | emit Registered(owner, name);
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/01-defender-meta-txs/contracts/SimpleRegistry.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity ^0.8.0;
3 |
4 | contract SimpleRegistry {
5 | event Registered(address indexed who, string name);
6 |
7 | mapping(address => string) public names;
8 | mapping(string => address) public owners;
9 |
10 | function register(string memory name) external {
11 | require(owners[name] == address(0), "Name taken");
12 | address owner = msg.sender;
13 | owners[name] = owner;
14 | names[owner] = name;
15 | emit Registered(owner, name);
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/01-defender-meta-txs/deploy.json:
--------------------------------------------------------------------------------
1 | {
2 | "MinimalForwarder": "0x1A8f6185Df1d2861d3012328Eb923e35Ca98Efed",
3 | "Registry": "0xC851CC8bf0ED0E5B3A7247b750451E9b75dd5f3A"
4 | }
--------------------------------------------------------------------------------
/01-defender-meta-txs/hardhat.config.js:
--------------------------------------------------------------------------------
1 | require('dotenv').config();
2 |
3 | require("@nomiclabs/hardhat-waffle");
4 | require("@nomiclabs/hardhat-ethers");
5 |
6 | task("accounts", "Prints the list of accounts", async () => {
7 | const accounts = await ethers.getSigners();
8 |
9 | for (const account of accounts) {
10 | console.log(account.address);
11 | }
12 | });
13 |
14 | /**
15 | * @type import('hardhat/config').HardhatUserConfig
16 | */
17 | module.exports = {
18 | solidity: "0.8.0",
19 | networks: {
20 | local: {
21 | url: 'http://localhost:8545'
22 | },
23 | xdai: {
24 | url: 'https://dai.poa.network',
25 | accounts: [process.env.PRIVATE_KEY],
26 | },
27 | mumbai: {
28 | url: 'https://matic-mumbai.chainstacklabs.com',
29 | accounts: [process.env.PRIVATE_KEY],
30 | }
31 | }
32 | };
33 |
--------------------------------------------------------------------------------
/01-defender-meta-txs/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "workshop-meta-txs",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "local:start": "hardhat node",
8 | "local:deploy": "hardhat run scripts/deploy.js --network local",
9 | "local:sign": "hardhat run scripts/sign.js --network local",
10 | "deploy": "hardhat run scripts/deploy.js --network xdai",
11 | "sign": "hardhat run scripts/sign.js --network xdai",
12 | "events": "hardhat run scripts/events.js --network xdai",
13 | "verify": "hardhat run scripts/verify.js --network xdai",
14 | "build": "rollup -c",
15 | "invoke": "bash ./scripts/invoke.sh",
16 | "upload": "yarn build && node scripts/upload.js",
17 | "relay": "node scripts/relay.js",
18 | "test": "NODE_ENV=test hardhat test"
19 | },
20 | "keywords": [],
21 | "author": "",
22 | "license": "ISC",
23 | "devDependencies": {
24 | "@nomiclabs/hardhat-ethers": "^2.0.1",
25 | "@nomiclabs/hardhat-waffle": "^2.0.1",
26 | "@openzeppelin/contracts": "^4.4.1",
27 | "@rollup/plugin-commonjs": "^17.1.0",
28 | "@rollup/plugin-json": "^4.1.0",
29 | "@rollup/plugin-node-resolve": "^11.1.1",
30 | "builtin-modules": "^3.2.0",
31 | "chai": "^4.3.0",
32 | "chai-as-promised": "^7.1.1",
33 | "create-react-app": "^4.0.2",
34 | "defender-autotask-client": "^1.4.1",
35 | "dotenv": "^8.2.0",
36 | "ethereum-waffle": "^3.2.2",
37 | "ethers": "^5.0.30",
38 | "hardhat": "^2.0.8",
39 | "rollup": "^2.38.5"
40 | },
41 | "dependencies": {
42 | "defender-relay-client": "^1.3.1",
43 | "eth-sig-util": "^3.0.1"
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/01-defender-meta-txs/rollup.config.js:
--------------------------------------------------------------------------------
1 | import resolve from '@rollup/plugin-node-resolve';
2 | import commonjs from '@rollup/plugin-commonjs';
3 | import json from '@rollup/plugin-json';
4 | import builtins from 'builtin-modules';
5 |
6 | export default {
7 | input: 'autotasks/relay/index.js',
8 | output: {
9 | file: 'build/relay/index.js',
10 | format: 'cjs',
11 | exports: 'auto',
12 | },
13 | plugins: [
14 | resolve({ preferBuiltins: true }),
15 | commonjs(),
16 | json({ compact: true }),
17 | ],
18 | external: [
19 | ...builtins,
20 | 'ethers',
21 | 'web3',
22 | 'axios',
23 | /^defender-relay-client(\/.*)?$/,
24 | ],
25 | };
26 |
--------------------------------------------------------------------------------
/01-defender-meta-txs/scripts/deploy.js:
--------------------------------------------------------------------------------
1 | const { ethers } = require('hardhat');
2 | const { writeFileSync } = require('fs');
3 |
4 | async function deploy(name, ...params) {
5 | const Contract = await ethers.getContractFactory(name);
6 | return await Contract.deploy(...params).then(f => f.deployed());
7 | }
8 |
9 | async function main() {
10 | const forwarder = await deploy('MinimalForwarder');
11 | const registry = await deploy("Registry", forwarder.address);
12 |
13 | writeFileSync('deploy.json', JSON.stringify({
14 | MinimalForwarder: forwarder.address,
15 | Registry: registry.address,
16 | }, null, 2));
17 |
18 | console.log(`MinimalForwarder: ${forwarder.address}\nRegistry: ${registry.address}`);
19 | }
20 |
21 | if (require.main === module) {
22 | main().then(() => process.exit(0))
23 | .catch(error => { console.error(error); process.exit(1); });
24 | }
--------------------------------------------------------------------------------
/01-defender-meta-txs/scripts/events.js:
--------------------------------------------------------------------------------
1 | const { ethers } = require('hardhat');
2 | const { readFileSync } = require('fs');
3 |
4 | function getInstance(name) {
5 | const address = JSON.parse(readFileSync('deploy.json'))[name];
6 | if (!address) throw new Error(`Contract ${name} not found in deploy.json`);
7 | return ethers.getContractFactory(name).then(f => f.attach(address));
8 | }
9 |
10 | async function main() {
11 | const registry = await getInstance("Registry");
12 | const events = await registry.queryFilter(registry.filters.Registered());
13 | console.log('Registrations')
14 | console.log('=============')
15 | console.log(events.map(e => `[${e.blockNumber}] ${e.args.who} => ${e.args.name}`).join('\n'));
16 | console.log();
17 | }
18 |
19 | if (require.main === module) {
20 | main().then(() => process.exit(0))
21 | .catch(error => { console.error(error); process.exit(1); });
22 | }
--------------------------------------------------------------------------------
/01-defender-meta-txs/scripts/invoke.sh:
--------------------------------------------------------------------------------
1 | source app/.env
2 | echo "Invoking $REACT_APP_WEBHOOK_URL..."
3 | curl -s -XPOST "$REACT_APP_WEBHOOK_URL" -d "@./tmp/request.json" -H "Content-Type: application/json" | jq -r 'if .status == "success" then (.result | fromjson | .txHash) else {result,message,status} end'
4 |
5 |
6 |
--------------------------------------------------------------------------------
/01-defender-meta-txs/scripts/relay.js:
--------------------------------------------------------------------------------
1 | const { handler } = require('../autotasks/relay/');
2 |
3 | // Run autotask code locally using the Relayer API key and secret
4 | if (require.main === module) {
5 | require('dotenv').config();
6 | const { RELAYER_API_KEY: apiKey, RELAYER_API_SECRET: apiSecret } = process.env;
7 | const payload = require('fs').readFileSync('tmp/request.json');
8 | handler({ apiKey, apiSecret, request: { body: JSON.parse(payload) } })
9 | .then(() => process.exit(0))
10 | .catch(error => { console.error(error); process.exit(1); });
11 | }
--------------------------------------------------------------------------------
/01-defender-meta-txs/scripts/sign.js:
--------------------------------------------------------------------------------
1 | const { ethers } = require('hardhat');
2 | const { signMetaTxRequest } = require('../src/signer');
3 | const { readFileSync, writeFileSync } = require('fs');
4 |
5 | const DEFAULT_NAME = 'sign-test';
6 |
7 | function getInstance(name) {
8 | const address = JSON.parse(readFileSync('deploy.json'))[name];
9 | if (!address) throw new Error(`Contract ${name} not found in deploy.json`);
10 | return ethers.getContractFactory(name).then(f => f.attach(address));
11 | }
12 |
13 | async function main() {
14 | const forwarder = await getInstance('MinimalForwarder');
15 | const registry = await getInstance("Registry");
16 |
17 | const { NAME: name, PRIVATE_KEY: signer } = process.env;
18 | const from = new ethers.Wallet(signer).address;
19 | console.log(`Signing registration of ${name || DEFAULT_NAME} as ${from}...`);
20 | const data = registry.interface.encodeFunctionData('register', [name || DEFAULT_NAME]);
21 | const result = await signMetaTxRequest(signer, forwarder, {
22 | to: registry.address, from, data
23 | });
24 |
25 | writeFileSync('tmp/request.json', JSON.stringify(result, null, 2));
26 | console.log(`Signature: `, result.signature);
27 | console.log(`Request: `, result.request);
28 | }
29 |
30 | if (require.main === module) {
31 | main().then(() => process.exit(0))
32 | .catch(error => { console.error(error); process.exit(1); });
33 | }
--------------------------------------------------------------------------------
/01-defender-meta-txs/scripts/upload.js:
--------------------------------------------------------------------------------
1 | const { AutotaskClient } = require('defender-autotask-client');
2 |
3 | async function uploadCode(autotaskId, apiKey, apiSecret) {
4 | const client = new AutotaskClient({ apiKey, apiSecret });
5 | await client.updateCodeFromFolder(autotaskId, './build/relay');
6 | }
7 |
8 | async function main() {
9 | require('dotenv').config();
10 | const { TEAM_API_KEY: apiKey, TEAM_API_SECRET: apiSecret, AUTOTASK_ID: autotaskId } = process.env;
11 | if (!autotaskId) throw new Error(`Missing autotask id`);
12 | await uploadCode(autotaskId, apiKey, apiSecret);
13 | console.log(`Code updated`);
14 | }
15 |
16 | if (require.main === module) {
17 | main().then(() => process.exit(0))
18 | .catch(error => { console.error(error); process.exit(1); });
19 | }
--------------------------------------------------------------------------------
/01-defender-meta-txs/scripts/verify.js:
--------------------------------------------------------------------------------
1 | const { ethers } = require('hardhat');
2 | const { readFileSync } = require('fs');
3 |
4 | function getInstance(name) {
5 | const address = JSON.parse(readFileSync('deploy.json'))[name];
6 | if (!address) throw new Error(`Contract ${name} not found in deploy.json`);
7 | return ethers.getContractFactory(name).then(f => f.attach(address));
8 | }
9 |
10 | async function main() {
11 | const forwarder = await getInstance('MinimalForwarder');
12 | console.log(`Testing request tmp/request.json on forwarder at ${forwarder.address}...`);
13 | const { request, signature } = JSON.parse(readFileSync('tmp/request.json'));
14 |
15 | try {
16 | const valid = await forwarder.verify(request, signature);
17 | console.log(`Signature ${signature} for request is${!valid ? ' not ' : ' '}valid`);
18 | } catch (err) {
19 | console.log(`Could not validate signature for request: ${err.message}`);
20 | }
21 | }
22 |
23 | if (require.main === module) {
24 | main().then(() => process.exit(0))
25 | .catch(error => { console.error(error); process.exit(1); });
26 | }
--------------------------------------------------------------------------------
/01-defender-meta-txs/slides/20210211 - Defender meta-txs workshop.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OpenZeppelin/workshops/ca3ef410f805d044aa6ac2b95be16104baf90fbf/01-defender-meta-txs/slides/20210211 - Defender meta-txs workshop.pdf
--------------------------------------------------------------------------------
/01-defender-meta-txs/src/forwarder.js:
--------------------------------------------------------------------------------
1 | /**
2 | * ABI of the MinimalForwarder contract
3 | */
4 | const ForwarderAbi = [{"inputs":[],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[{"components":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"},{"internalType":"uint256","name":"gas","type":"uint256"},{"internalType":"uint256","name":"nonce","type":"uint256"},{"internalType":"bytes","name":"data","type":"bytes"}],"internalType":"struct MinimalForwarder.ForwardRequest","name":"req","type":"tuple"},{"internalType":"bytes","name":"signature","type":"bytes"}],"name":"execute","outputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"bytes","name":"","type":"bytes"}],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"}],"name":"getNonce","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"components":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"},{"internalType":"uint256","name":"gas","type":"uint256"},{"internalType":"uint256","name":"nonce","type":"uint256"},{"internalType":"bytes","name":"data","type":"bytes"}],"internalType":"struct MinimalForwarder.ForwardRequest","name":"req","type":"tuple"},{"internalType":"bytes","name":"signature","type":"bytes"}],"name":"verify","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"}];
5 |
6 | module.exports = {
7 | ForwarderAbi
8 | }
9 |
--------------------------------------------------------------------------------
/01-defender-meta-txs/src/signer.js:
--------------------------------------------------------------------------------
1 | const ethSigUtil = require('eth-sig-util');
2 |
3 | const EIP712Domain = [
4 | { name: 'name', type: 'string' },
5 | { name: 'version', type: 'string' },
6 | { name: 'chainId', type: 'uint256' },
7 | { name: 'verifyingContract', type: 'address' }
8 | ];
9 |
10 | const ForwardRequest = [
11 | { name: 'from', type: 'address' },
12 | { name: 'to', type: 'address' },
13 | { name: 'value', type: 'uint256' },
14 | { name: 'gas', type: 'uint256' },
15 | { name: 'nonce', type: 'uint256' },
16 | { name: 'data', type: 'bytes' },
17 | ];
18 |
19 | function getMetaTxTypeData(chainId, verifyingContract) {
20 | return {
21 | types: {
22 | EIP712Domain,
23 | ForwardRequest,
24 | },
25 | domain: {
26 | name: 'MinimalForwarder',
27 | version: '0.0.1',
28 | chainId,
29 | verifyingContract,
30 | },
31 | primaryType: 'ForwardRequest',
32 | }
33 | };
34 |
35 | async function signTypedData(signer, from, data) {
36 | // If signer is a private key, use it to sign
37 | if (typeof(signer) === 'string') {
38 | const privateKey = Buffer.from(signer.replace(/^0x/, ''), 'hex');
39 | return ethSigUtil.signTypedMessage(privateKey, { data });
40 | }
41 |
42 | // Otherwise, send the signTypedData RPC call
43 | // Note that hardhatvm and metamask require different EIP712 input
44 | const isHardhat = data.domain.chainId == 31337;
45 | const [method, argData] = isHardhat
46 | ? ['eth_signTypedData', data]
47 | : ['eth_signTypedData_v4', JSON.stringify(data)]
48 | return await signer.send(method, [from, argData]);
49 | }
50 |
51 | async function buildRequest(forwarder, input) {
52 | const nonce = await forwarder.getNonce(input.from).then(nonce => nonce.toString());
53 | return { value: 0, gas: 1e6, nonce, ...input };
54 | }
55 |
56 | async function buildTypedData(forwarder, request) {
57 | const chainId = await forwarder.provider.getNetwork().then(n => n.chainId);
58 | const typeData = getMetaTxTypeData(chainId, forwarder.address);
59 | return { ...typeData, message: request };
60 | }
61 |
62 | async function signMetaTxRequest(signer, forwarder, input) {
63 | const request = await buildRequest(forwarder, input);
64 | const toSign = await buildTypedData(forwarder, request);
65 | const signature = await signTypedData(signer, input.from, toSign);
66 | return { signature, request };
67 | }
68 |
69 | module.exports = {
70 | signMetaTxRequest,
71 | buildRequest,
72 | buildTypedData,
73 | };
74 |
--------------------------------------------------------------------------------
/01-defender-meta-txs/test/contracts/Registry.test.js:
--------------------------------------------------------------------------------
1 | const { expect } = require("chai");
2 | const { ethers } = require("hardhat");
3 | const { signMetaTxRequest } = require("../../src/signer");
4 |
5 | async function deploy(name, ...params) {
6 | const Contract = await ethers.getContractFactory(name);
7 | return await Contract.deploy(...params).then(f => f.deployed());
8 | }
9 |
10 | describe("contracts/Registry", function() {
11 | beforeEach(async function() {
12 | this.forwarder = await deploy('MinimalForwarder');
13 | this.registry = await deploy("Registry", this.forwarder.address);
14 | this.accounts = await ethers.getSigners();
15 | });
16 |
17 | it("registers a name directly", async function() {
18 | const sender = this.accounts[1];
19 | const registry = this.registry.connect(sender);
20 |
21 | const receipt = await registry.register('defender').then(tx => tx.wait());
22 | expect(receipt.events[0].event).to.equal('Registered');
23 |
24 | expect(await registry.owners('defender')).to.equal(sender.address);
25 | expect(await registry.names(sender.address)).to.equal('defender');
26 | });
27 |
28 | it("registers a name via a meta-tx", async function() {
29 | const signer = this.accounts[2];
30 | const relayer = this.accounts[3];
31 | const forwarder = this.forwarder.connect(relayer);
32 | const registry = this.registry;
33 |
34 | const { request, signature } = await signMetaTxRequest(signer.provider, forwarder, {
35 | from: signer.address,
36 | to: registry.address,
37 | data: registry.interface.encodeFunctionData('register', ['meta-txs']),
38 | });
39 |
40 | await forwarder.execute(request, signature).then(tx => tx.wait());
41 |
42 | expect(await registry.owners('meta-txs')).to.equal(signer.address);
43 | expect(await registry.names(signer.address)).to.equal('meta-txs');
44 | });
45 | });
46 |
--------------------------------------------------------------------------------
/01-defender-meta-txs/test/contracts/SimpleRegistry.test.js:
--------------------------------------------------------------------------------
1 | const { expect } = require("chai");
2 | const { ethers } = require("hardhat");
3 |
4 | async function deploy(name, ...params) {
5 | const Contract = await ethers.getContractFactory(name);
6 | return await Contract.deploy(...params).then(f => f.deployed());
7 | }
8 |
9 | describe("contracts/SimpleRegistry", function() {
10 | beforeEach(async function() {
11 | this.registry = await deploy('SimpleRegistry');
12 | this.accounts = await ethers.getSigners();
13 | });
14 |
15 | it("registers a name", async function() {
16 | const sender = this.accounts[1];
17 | const registry = this.registry.connect(sender);
18 |
19 | const receipt = await registry.register('defender').then(tx => tx.wait());
20 |
21 | expect(receipt.events[0].event).to.equal('Registered');
22 | expect(await registry.owners('defender')).to.equal(sender.address);
23 | expect(await registry.names(sender.address)).to.equal('defender');
24 | });
25 | });
26 |
--------------------------------------------------------------------------------
/01-defender-meta-txs/test/src/relay.test.js:
--------------------------------------------------------------------------------
1 | const { expect } = require("chai").use(require('chai-as-promised'));
2 | const { ethers } = require("hardhat");
3 | const { signMetaTxRequest } = require("../../src/signer");
4 | const { relay } = require('../../autotasks/relay');
5 |
6 | async function deploy(name, ...params) {
7 | const Contract = await ethers.getContractFactory(name);
8 | return await Contract.deploy(...params).then(f => f.deployed());
9 | }
10 |
11 | describe("autotasks/relay", function() {
12 | beforeEach(async function() {
13 | this.forwarder = await deploy('MinimalForwarder');
14 | this.registry = await deploy("Registry", this.forwarder.address);
15 | this.accounts = await ethers.getSigners();
16 | this.signer = this.accounts[2];
17 | });
18 |
19 | it("registers a name via a meta-tx", async function() {
20 | const { forwarder, registry, signer } = this;
21 |
22 | const { request, signature } = await signMetaTxRequest(signer.provider, forwarder, {
23 | from: signer.address,
24 | to: registry.address,
25 | data: registry.interface.encodeFunctionData('register', ['meta-txs']),
26 | });
27 |
28 | const whitelist = [registry.address]
29 | await relay(forwarder, request, signature, whitelist);
30 |
31 | expect(await registry.owners('meta-txs')).to.equal(signer.address);
32 | expect(await registry.names(signer.address)).to.equal('meta-txs');
33 | });
34 |
35 | it("refuses to send to non-whitelisted address", async function() {
36 | const { forwarder, registry, signer } = this;
37 |
38 | const { request, signature } = await signMetaTxRequest(signer.provider, forwarder, {
39 | from: signer.address,
40 | to: registry.address,
41 | data: registry.interface.encodeFunctionData('register', ['meta-txs']),
42 | });
43 |
44 | const whitelist = [];
45 | await expect(
46 | relay(forwarder, request, signature, whitelist)
47 | ).to.be.rejectedWith(/rejected/i);
48 | });
49 |
50 | it("refuses to send incorrect signature", async function() {
51 | const { forwarder, registry, signer } = this;
52 |
53 | const { request, signature } = await signMetaTxRequest(signer.provider, forwarder, {
54 | from: signer.address,
55 | to: registry.address,
56 | data: registry.interface.encodeFunctionData('register', ['meta-txs']),
57 | nonce: 5,
58 | });
59 |
60 | const whitelist = [registry.address]
61 | await expect(
62 | relay(forwarder, request, signature, whitelist)
63 | ).to.be.rejectedWith(/invalid/i);
64 | });
65 | });
66 |
--------------------------------------------------------------------------------
/02-contracts-clone/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | artifacts
3 | cache
4 | deploy.*.json
5 | /.env
6 | tmp
7 | build
8 |
--------------------------------------------------------------------------------
/02-contracts-clone/README.md:
--------------------------------------------------------------------------------
1 | # Contract Clones Workshop
2 |
3 | Code for the workshop on Cheap smart contracts deployments through Clones using [OpenZeppelin Contracts](https://openzeppelin.com/contracts/)
4 |
5 | This project showcases usage of the clone library in order to improve the deployment of families of smart contracts. Clones, as described in [ERC1167](https://eips.ethereum.org/EIPS/eip-1167), are very small, and cheap to deploy, smart-contract that delegates all incoming calls to a master functionality contract. The address of this master contract being stored directly in the contract code, no `sload` is required.
6 |
7 | The gas report produced by the tests shows the gas savings one can expect from using clones.
8 |
9 | ## Structure
10 |
11 | - `contracts/1-ERC20`: Comparison of three different deployment mechanisms for ERC20 tokens.
12 | - `contracts/2-uniswap`: Fork of [uniswap](https://uniswap.org/)'s `UniswapV2Factory` that uses EIP-1167 clones to deploy `UniswapV2Pair`
13 | - `contracts/3-argent`: Fork of [argent](https://www.argent.xyz/)'s `WalletFactory` that uses EIP-1167 clones to deploy `WalletProxy`
14 | - `test`: Tests for contracts.
15 | - `hardhart.config.js`: Hardhat configuration, including compiler optimization options.
16 |
17 | ## Scripts
18 |
19 | - `npm run compile`: Compile the contracts.
20 | - `npm run test`: Run the tests and produce a gas usage report.
21 |
22 | ## Examples
23 |
24 | 1. **ERC20**
25 |
26 | - Description:
27 |
28 | In this example, we deploy ERC20 using different mechanisms. This allows us to compare the deployment and usage costs associated with the different deployment patterns.
29 |
30 | - Gas report:
31 |
32 | | Contract | Methods | Gas |
33 | | ------------ | -------------- | --------: |
34 | | FactoryNaive | constructor | 1,245,959 |
35 | | FactoryProxy | constructor | 1,675,235 |
36 | | FactoryClone | constructor | 1,305,848 |
37 | | FactoryNaive | createToken | 1,179,977 |
38 | | FactoryProxy | createToken | 368,401 |
39 | | FactoryClone | createToken | 209,109 |
40 | | FactoryNaive | ERC20.transfer | 51,092 |
41 | | FactoryProxy | ERC20.transfer | 52,776 |
42 | | FactoryClone | ERC20.transfer | 51,870 |
43 |
44 | 2. **UniswapV2**
45 |
46 | - Description:
47 |
48 | Uniswap is one of the most widely adopted AMM-based decentralized exchanges in the ethereum ecosystem. Version 2, which is the latest version to date relies on a large set of liquidity pairs and a routing mechanism that allows the execution of more complex operations that combines multiple pairs.
49 |
50 | A uniswap pair corresponds to a pair of ERC20 compatible assets and is represented by an independent smart-contract, which holds the underlying asset and keeps track of liquidity providers for this particular pair.
51 |
52 | Anyone can create a uniswap pair contract for any ERC20s he desires. However, this operation has a significant gas cost associated with deploying and configuring the pair contract. This operation can be made cheaper using EIP-1167 clones instead of full-fledged contracts.
53 |
54 | - Gas report:
55 |
56 | | Contract | Methods | Gas |
57 | | ---------------------- | ---------- | --------: |
58 | | UniswapV2Factory | createPair | 2,020,039 |
59 | | UniswapV2FactoryClones | createPair | 218,099 |
60 |
61 | - Comment:
62 |
63 | While deployment cost is greatly reduced, each transaction (including internal transaction) to a pair will require an additional delegate call from the proxy to the `UniswapV2Pair` implementation. These delegate calls, while cheap on a "per-transaction" basis, will eventually make the clone option more expensive to the community.
64 |
65 | It is a good thing that UniswapV2 doesn't use clones for pairs like `ETH/DAI` or `ETH/USDC` that see a lot of volumes. However, some low-traffic pairs would have benefited from a cheap deployment.
66 |
67 | - Relevant contracts:
68 | - [UniswapV2Factory](https://etherscan.io/address/0x5c69bee701ef814a2b6a3edd4b1652cb9cc5aa6f#code)
69 | - [UniswapV2Pair for DAI/WETH](https://etherscan.io/address/0xa478c2975ab1ea89e8196811f51a7b7ade33eb11#code)
70 |
71 | 3. **Argent**
72 |
73 | - Description:
74 |
75 | Argent is an ethereum smart-contract-based non-custodial wallet. Its integration of DeFi primitives and its innovative social recovery mechanisms make it one of the best solutions available today.
76 |
77 | Each argent user has a dedicated smart-wallet that they control through meta-transactions. One of the consequences of this design is that onboarding new users requires deploying a smart contract for them. Argent already uses proxies to reduce the cost of new wallet creation, but they could go even further using EIP-1167 clones.
78 |
79 | - Gas report:
80 |
81 | | Contract | Methods | Gas |
82 | | ------------------- | -------------------------- | ------: |
83 | | WalletFactory | createCounterfactualWallet | 322,302 |
84 | | WalletFactoryClones | createCounterfactualWallet | 265,422 |
85 |
86 | - Relevant contracts:
87 | - [WalletFactory (new with about 18.9k wallets)](https://etherscan.io/address/0x40c84310ef15b0c0e5c69d25138e0e16e8000fe9#code)
88 | - [WalletFactory (old with about 17.5k wallets)](https://etherscan.io/address/0x851cc731ce1613ae4fd8ec7f61f4b350f9ce1020#code)
89 |
--------------------------------------------------------------------------------
/02-contracts-clone/contracts/1-ERC20/FactoryClone.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | pragma solidity ^0.6.0;
4 |
5 | import "@openzeppelin/contracts-upgradeable/presets/ERC20PresetFixedSupplyUpgradeable.sol";
6 | import "@openzeppelin/contracts/proxy/Clones.sol";
7 |
8 | contract FactoryClone {
9 | address immutable tokenImplementation;
10 |
11 | constructor() public {
12 | tokenImplementation = address(new ERC20PresetFixedSupplyUpgradeable());
13 | }
14 |
15 | function createToken(string calldata name, string calldata symbol, uint256 initialSupply) external returns (address) {
16 | address clone = Clones.clone(tokenImplementation);
17 | ERC20PresetFixedSupplyUpgradeable(clone).initialize(name, symbol, initialSupply, msg.sender);
18 | return clone;
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/02-contracts-clone/contracts/1-ERC20/FactoryNaive.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | pragma solidity ^0.6.0;
4 |
5 | import "@openzeppelin/contracts-upgradeable/presets/ERC20PresetFixedSupplyUpgradeable.sol";
6 |
7 | contract FactoryNaive {
8 | function createToken(string calldata name, string calldata symbol, uint256 initialSupply) external returns (address) {
9 | ERC20PresetFixedSupplyUpgradeable token = new ERC20PresetFixedSupplyUpgradeable();
10 | token.initialize(name, symbol, initialSupply, msg.sender);
11 | return address(token);
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/02-contracts-clone/contracts/1-ERC20/FactoryProxy.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | pragma solidity ^0.6.0;
4 |
5 | import "@openzeppelin/contracts-upgradeable/presets/ERC20PresetFixedSupplyUpgradeable.sol";
6 | import "@openzeppelin/contracts/proxy/UpgradeableProxy.sol";
7 |
8 | contract FactoryProxy {
9 | address immutable tokenImplementation;
10 |
11 | constructor() public {
12 | tokenImplementation = address(new ERC20PresetFixedSupplyUpgradeable());
13 | }
14 |
15 | function createToken(string calldata name, string calldata symbol, uint256 initialSupply) external returns (address) {
16 | UpgradeableProxy proxy = new UpgradeableProxy(
17 | tokenImplementation,
18 | abi.encodeWithSelector(ERC20PresetFixedSupplyUpgradeable(0).initialize.selector, name, symbol, initialSupply, msg.sender)
19 | );
20 | return address(proxy);
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/02-contracts-clone/contracts/2-uniswap/UniswapV2FactoryClones.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | pragma solidity ^0.6.0;
4 |
5 | import "@openzeppelin/contracts/proxy/Clones.sol";
6 | import "@uniswap/v2-core/contracts/interfaces/IUniswapV2Factory.sol";
7 | import "@uniswap/v2-core/contracts/interfaces/IUniswapV2Pair.sol";
8 |
9 | contract UniswapV2FactoryClones is IUniswapV2Factory {
10 | address public uniswapV2PairImplementation;
11 | address public override feeTo;
12 | address public override feeToSetter;
13 |
14 | mapping(address => mapping(address => address)) public override getPair;
15 | address[] public override allPairs;
16 |
17 | constructor(address _uniswapV2PairImplementation, address _feeToSetter) public {
18 | uniswapV2PairImplementation = _uniswapV2PairImplementation;
19 | feeToSetter = _feeToSetter;
20 | }
21 |
22 | function allPairsLength() external view override returns (uint) {
23 | return allPairs.length;
24 | }
25 |
26 | function createPair(address tokenA, address tokenB) external override returns (address pair) {
27 | require(tokenA != tokenB, "UniswapV2: IDENTICAL_ADDRESSES");
28 | (address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
29 | require(token0 != address(0), "UniswapV2: ZERO_ADDRESS");
30 | require(getPair[token0][token1] == address(0), "UniswapV2: PAIR_EXISTS"); // single check is sufficient
31 | bytes32 salt = keccak256(abi.encodePacked(token0, token1));
32 | pair = Clones.cloneDeterministic(uniswapV2PairImplementation, salt);
33 | IUniswapV2Pair(pair).initialize(token0, token1);
34 | getPair[token0][token1] = pair;
35 | getPair[token1][token0] = pair; // populate mapping in the reverse direction
36 | allPairs.push(pair);
37 | emit PairCreated(token0, token1, pair, allPairs.length);
38 | }
39 |
40 | function setFeeTo(address _feeTo) external override {
41 | require(msg.sender == feeToSetter, "UniswapV2: FORBIDDEN");
42 | feeTo = _feeTo;
43 | }
44 |
45 | function setFeeToSetter(address _feeToSetter) external override {
46 | require(msg.sender == feeToSetter, "UniswapV2: FORBIDDEN");
47 | feeToSetter = _feeToSetter;
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/02-contracts-clone/contracts/2-uniswap/import.sol:
--------------------------------------------------------------------------------
1 | pragma solidity =0.5.16;
2 |
3 | import "@uniswap/v2-core/contracts/UniswapV2Factory.sol";
4 |
--------------------------------------------------------------------------------
/02-contracts-clone/contracts/3-argent/import_0.5.sol:
--------------------------------------------------------------------------------
1 | pragma solidity ^0.5.4;
2 |
3 | import "argent-contracts/contracts/infrastructure_0.5/ModuleRegistry.sol";
4 | import "argent-contracts/contracts/infrastructure_0.5/storage/GuardianStorage.sol";
5 |
--------------------------------------------------------------------------------
/02-contracts-clone/contracts/3-argent/import_0.6.sol:
--------------------------------------------------------------------------------
1 | pragma solidity ^0.6.12;
2 |
3 | import "argent-contracts/contracts/infrastructure/WalletFactory.sol";
4 | import "argent-contracts/contracts/modules/VersionManager.sol";
5 | import "argent-contracts/contracts/wallet/BaseWallet.sol";
6 |
--------------------------------------------------------------------------------
/02-contracts-clone/hardhat.config.js:
--------------------------------------------------------------------------------
1 | require('@nomiclabs/hardhat-waffle');
2 | require('@nomiclabs/hardhat-ethers');
3 | require('hardhat-gas-reporter');
4 |
5 | const settings = {
6 | optimizer: {
7 | enabled: true,
8 | runs: 200,
9 | },
10 | };
11 |
12 | /**
13 | * @type import('hardhat/config').HardhatUserConfig
14 | */
15 | module.exports = {
16 | solidity: {
17 | compilers: [
18 | { version: '0.5.16', settings },
19 | { version: '0.6.12', settings },
20 | { version: '0.7.6', settings },
21 | ],
22 | },
23 | gasReporter: {
24 | enable: true,
25 | currency: 'USD',
26 | },
27 | };
28 |
--------------------------------------------------------------------------------
/02-contracts-clone/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "workshop-clone",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "compile": "hardhat compile",
8 | "test": "hardhat test"
9 | },
10 | "author": "OpenZeppelin",
11 | "license": "MIT",
12 | "repository": {
13 | "type": "git",
14 | "url": "git+https://github.com/OpenZeppelin/workshops.git"
15 | },
16 | "homepage": "https://github.com/OpenZeppelin/workshops#readme",
17 | "bugs": {
18 | "url": "https://github.com/OpenZeppelin/workshops/issues"
19 | },
20 | "dependencies": {
21 | "@openzeppelin/contracts": "^3.4.0",
22 | "@openzeppelin/contracts-upgradeable": "^3.4.0",
23 | "@uniswap/v2-core": "^1.0.1",
24 | "argent-contracts": "git+https://github.com/argentlabs/argent-contracts.git"
25 | },
26 | "devDependencies": {
27 | "@nomiclabs/hardhat-ethers": "^2.0.1",
28 | "@nomiclabs/hardhat-waffle": "^2.0.1",
29 | "hardhat": "^2.0.10",
30 | "hardhat-gas-reporter": "^1.0.4"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/02-contracts-clone/slides/20210227 - Contracts clones workshop.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OpenZeppelin/workshops/ca3ef410f805d044aa6ac2b95be16104baf90fbf/02-contracts-clone/slides/20210227 - Contracts clones workshop.pdf
--------------------------------------------------------------------------------
/02-contracts-clone/test/1-ERC20.test.js:
--------------------------------------------------------------------------------
1 |
2 | async function deploy(name, ...params) {
3 | const Contract = await ethers.getContractFactory(name);
4 | return await Contract.deploy(...params).then(f => f.deployed());
5 | }
6 |
7 | describe('factories', function() {
8 | for (const name of [ 'FactoryNaive', 'FactoryProxy', 'FactoryClone']) {
9 | describe(name, function() {
10 | before(async function() {
11 | this.accounts = await ethers.getSigners();
12 | this.factory = await deploy(name);
13 | });
14 |
15 | it('factory deployment cost', async function() {
16 | await this.factory.deployTransaction.wait();
17 | });
18 |
19 | it('wallet deployment cost', async function() {
20 | const tx1 = await this.factory.createToken('name', 'symbol', 1000, { from: this.accounts[0].address });
21 | const { gasUsed: createGasUsed, events } = await tx1.wait();
22 | const { address } = events.find(Boolean);
23 | console.log(`${name}.createToken: ${createGasUsed.toString()}`);
24 |
25 | const { interface } = await ethers.getContractFactory('ERC20PresetFixedSupplyUpgradeable');
26 | const instance = new ethers.Contract(address, interface, this.accounts[0]);
27 | const tx2 = await instance.transfer(this.accounts[1].address, 100, { from: this.accounts[0].address });
28 | const { gasUsed: transferGasUsed } = await tx2.wait();
29 | console.log(`ERC20.transfer: ${transferGasUsed.toString()}`);
30 | });
31 | });
32 | }
33 | });
34 |
--------------------------------------------------------------------------------
/02-contracts-clone/test/2-Uniswap.test.js:
--------------------------------------------------------------------------------
1 | async function deploy(name, ...params) {
2 | const Contract = await ethers.getContractFactory(name);
3 | return await Contract.deploy(...params).then(f => f.deployed());
4 | }
5 |
6 | describe('Uniswap', function() {
7 | before(async function() {
8 | this.accounts = await ethers.getSigners();
9 | });
10 |
11 | describe('Native', function() {
12 | before(async function() {
13 | this.factory = await deploy('UniswapV2Factory', '0x18e433c7bf8a2e1d0197ce5d8f9afada1a771360');
14 | });
15 |
16 | it('create pair', async function() {
17 | await this.factory.createPair(
18 | this.accounts[1].address,
19 | this.accounts[2].address,
20 | );
21 | });
22 | });
23 |
24 | describe('With clones', function() {
25 | before(async function() {
26 | this.template = await deploy('UniswapV2PairClones');
27 | this.factory = await deploy('UniswapV2FactoryClones', this.template.address, '0x18e433c7bf8a2e1d0197ce5d8f9afada1a771360');
28 | });
29 |
30 | it('create pair', async function() {
31 | await this.factory.createPair(
32 | this.accounts[1].address,
33 | this.accounts[2].address,
34 | );
35 | });
36 | });
37 | });
38 |
--------------------------------------------------------------------------------
/02-contracts-clone/test/3-Argent.test.js:
--------------------------------------------------------------------------------
1 | async function deploy(name, ...params) {
2 | const Contract = await ethers.getContractFactory(name);
3 | return await Contract.deploy(...params).then(f => f.deployed());
4 | }
5 |
6 | describe('Argent', function() {
7 | before(async function () {
8 | this.implementation = await deploy('BaseWallet');
9 | this.moduleRegistry = await deploy('ModuleRegistry');
10 | this.guardianStorage = await deploy('GuardianStorage');
11 | this.versionmanager = await deploy(
12 | 'VersionManager',
13 | this.moduleRegistry.address,
14 | this.guardianStorage.address,
15 | ethers.constants.AddressZero,
16 | ethers.constants.AddressZero,
17 | ethers.constants.AddressZero);
18 | await this.versionmanager.addVersion([], []);
19 |
20 | this.accounts = await ethers.getSigners();
21 | this.infrastructure = this.accounts[0];
22 | this.owner = this.accounts[1];
23 | this.guardian = this.accounts[2];
24 | this.other = this.accounts[3];
25 | this.refundAddress = this.accounts[4];
26 | });
27 |
28 | for (const name of [ 'WalletFactory', 'WalletFactoryClones' ]) {
29 | describe(name, function() {
30 | before(async function() {
31 | this.factory = await deploy(
32 | name,
33 | this.implementation.address,
34 | this.guardianStorage.address,
35 | this.refundAddress.address,
36 | );
37 | await this.factory.addManager(this.infrastructure.address);
38 | });
39 |
40 | it('create wallet', async function() {
41 | await this.factory.createCounterfactualWallet(
42 | this.owner.address,
43 | this.versionmanager.address,
44 | this.guardian.address,
45 | ethers.utils.randomBytes(20),
46 | 1,
47 | 0,
48 | ethers.constants.AddressZero,
49 | "0x",
50 | "0x",
51 | );
52 | });
53 | });
54 | }
55 | });
56 |
--------------------------------------------------------------------------------
/03-defender-service-monitoring/README.md:
--------------------------------------------------------------------------------
1 | # Service Monitoring and Emergency Response with Defender Workshop
2 |
3 | Code for the workshop on Service Monitoring and Emergency Response with [Defender](https://openzeppelin.com/defender)
4 |
--------------------------------------------------------------------------------
/03-defender-service-monitoring/code/greeting-example/Greeter.sol:
--------------------------------------------------------------------------------
1 | pragma solidity >=0.7.0 <0.8.0;
2 |
3 | contract Greeter {
4 |
5 | bool paused;
6 | mapping(address => bool) public admins;
7 |
8 | event Greeting(string text, address sender);
9 |
10 | constructor() {
11 | admins[msg.sender] = true;
12 | }
13 |
14 | modifier notPaused {
15 | require(!paused);
16 | _;
17 | }
18 |
19 | modifier adminOnly {
20 | require(admins[msg.sender]);
21 | _;
22 | }
23 |
24 | function addAdmin(address addr) adminOnly public {
25 | admins[addr] = true;
26 | }
27 |
28 | function dropAdmin(address addr) adminOnly public {
29 | admins[addr] = false;
30 | }
31 |
32 | function pause() adminOnly public {
33 | paused = true;
34 | }
35 |
36 | function unpause() adminOnly public {
37 | paused = false;
38 | }
39 |
40 | function greet(string memory text) notPaused public {
41 | require(keccak256(bytes(text)) != keccak256(bytes("invalid")));
42 | emit Greeting(text, msg.sender);
43 | }
44 |
45 | }
46 |
--------------------------------------------------------------------------------
/03-defender-service-monitoring/code/greeting-example/autotask-pause-greeter.js:
--------------------------------------------------------------------------------
1 | const { Relayer } = require('defender-relay-client');
2 | const { ethers } = require("ethers");
3 | const { DefenderRelaySigner, DefenderRelayProvider } = require('defender-relay-client/lib/ethers');
4 |
5 | const abi = [{"inputs":[],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"string","name":"text","type":"string"},{"indexed":false,"internalType":"address","name":"sender","type":"address"}],"name":"Greeting","type":"event"},{"inputs":[{"internalType":"address","name":"addr","type":"address"}],"name":"addAdmin","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"admins","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"addr","type":"address"}],"name":"dropAdmin","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"string","name":"text","type":"string"}],"name":"greet","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"pause","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"unpause","outputs":[],"stateMutability":"nonpayable","type":"function"}];
6 |
7 | exports.handler = async function(request) {
8 | const greeterAddress = "0x42307F2201658D49801c37eb82F5Db0Cb0546D10";
9 | const client = new Relayer(request);
10 | const relayer = await client.getRelayer();
11 | const provider = new DefenderRelayProvider(request);
12 | const signer = new DefenderRelaySigner(request, provider, {
13 | speed: 'fast',
14 | from: relayer.address,
15 | });
16 |
17 | const greeter = new ethers.Contract(greeterAddress, abi, signer);
18 | await greeter.pause();
19 |
20 | return `paused ${greeter.address}`
21 | }
22 |
--------------------------------------------------------------------------------
/03-defender-service-monitoring/code/yearn-example/autotask-loss-detector.js:
--------------------------------------------------------------------------------
1 | const {
2 | Relayer
3 | } = require('defender-relay-client');
4 | const {
5 | ethers
6 | } = require("ethers");
7 | const axios = require('axios');
8 | const {
9 | DefenderRelaySigner,
10 | DefenderRelayProvider
11 | } = require('defender-relay-client/lib/ethers');
12 | const divider = {
13 | type: 'divider'
14 | };
15 |
16 | function markdown(msg) {
17 | return {
18 | type: 'section',
19 | text: {
20 | type: 'mrkdwn',
21 | text: msg,
22 | },
23 | }
24 | }
25 |
26 | function txLink(hash) {
27 | return `https://etherscan.io/tx/${hash}`
28 | }
29 | async function sendLossAlert(payload, sentinelEvent, loss) {
30 | const notification = {
31 | blocks: [divider, markdown('❗*VAULT LOSS DETECTED*❗'), divider, markdown(`Address: ${sentinelEvent.sentinel.address}`), markdown(`Amount: ${loss}`), markdown(`Risky Transaction: ${txLink(sentinelEvent.transaction.transactionHash)}`)],
32 | };
33 | const url = payload.secrets.slackWebhookUrl;
34 | await axios.post(url, notification, {
35 | headers: {
36 | 'Content-Type': 'application/json'
37 | }
38 | });
39 | }
40 |
41 | // 1 Million DAI
42 | const threshold = ethers.BigNumber.from("-1000000000000000000000000")
43 |
44 | // receives event from sentinel
45 | exports.handler = async function (payload) {
46 | // init
47 | const provider = new DefenderRelayProvider(payload);
48 | const evt = payload.request.body;
49 |
50 | // init vault contract
51 | const abi = evt.sentinel.abi;
52 | const address = evt.sentinel.address;
53 | const yVault = new ethers.Contract(address, abi, provider);
54 |
55 | // get balance for this block and previous block
56 | const block = await provider.getBlock(evt.blockHash)
57 | const currentBlockBalance = await yVault.balance({
58 | blockTag: block.number
59 | });
60 | const prevBlockBalance = await yVault.balance({
61 | blockTag: block.number - 1
62 | })
63 |
64 | // send alert if loss exceeds threshold
65 | const delta = currentBlockBalance.sub(prevBlockBalance);
66 | const exceedsThreshold = delta.lte(threshold);
67 | if (exceedsThreshold) {
68 | await sendLossAlert(payload, evt, delta);
69 | }
70 |
71 | // for logging
72 | return {
73 | currentBlockBalance,
74 | prevBlockBalance,
75 | delta,
76 | exceedsThreshold
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/03-defender-service-monitoring/slides/20210311 - Service Monitoring and Emergency Response Workshop.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OpenZeppelin/workshops/ca3ef410f805d044aa6ac2b95be16104baf90fbf/03-defender-service-monitoring/slides/20210311 - Service Monitoring and Emergency Response Workshop.pdf
--------------------------------------------------------------------------------
/04-roles/slides.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OpenZeppelin/workshops/ca3ef410f805d044aa6ac2b95be16104baf90fbf/04-roles/slides.pdf
--------------------------------------------------------------------------------
/05-upgrades-management/code/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .env
3 |
4 | #Hardhat files
5 | cache
6 | artifacts
7 |
--------------------------------------------------------------------------------
/05-upgrades-management/code/README.md:
--------------------------------------------------------------------------------
1 | # Managing Smart Contract Upgrades Workshop
2 |
3 | This folder contains the code used during the Managing Smart Contract Upgrades Workshop we led in Apr 22nd, 2021.
4 |
5 | It is a Hardhat project, with some iterations on a simple `Box` contract, and a handful of Hardhat scripts to manage its upgrades.
6 |
7 | ## Installing
8 |
9 | Check out this folder to your local environment, and run:
10 |
11 | `npm install`
12 |
13 | ## Configuring
14 |
15 | All necessary configuration variables are sourced through a `.env` file at the root level of this folder.
16 |
17 | Create a `.env` file and make sure it defines the env required variables. Your `.env` should look like this:
18 |
19 | ```
20 | MNEMONIC=
21 | INFURA_API_KEY=
22 | DEFENDER_TEAM_API_KEY=
23 | DEFENDER_TEAM_API_SECRET_KEY=
24 | ```
25 |
26 | Some resources to help you get each of this:
27 |
28 | 1. MNEMONIC:
29 | 1. https://ethereum.org/en/glossary/#hd-wallet-seed
30 | 2. https://ethereum.org/en/developers/docs/accounts/
31 | 2. INFURA_API_KEY:
32 | 1. https://infura.io/docs/ethereum#section/Securing-Your-Credentials
33 | 3. DEFENDER Team API keys: https://docs.openzeppelin.com/defender/guide-upgrades#create-defender-team-api-key
34 |
35 | ## Deploying an upgradeable contract
36 |
37 | To deploy the initial version of Box simply run:
38 |
39 | `npx hardhat run --network rinkeby scripts/deploy.js`.
40 |
41 | If it succeeds, the script will print the address of your contract (aka the Proxy) to the console. Write down that address, as it is the one through which you'll manage your contract from now on.
42 |
43 | ## Upgrading to BoxV2
44 |
45 | In `scripts/upgrade.js`, replace the proxy address with the one you got from the deployment script, then run:
46 |
47 | `npx hardhat run --network rinkeby scripts/upgrade.js`
48 |
49 | ## Transferring upgrade rights to a multisig
50 |
51 | Go to `rinkeby.gnosis-safe.io` and create a multisig. Then copy its address and, in `scripts/transfer-ownership.js`, replace the `gnosisSafe` variable with your own multisig address. Be EXTRA CAREFUL in this step, if you call this script with the wrong address, you'll lose control of your contract's upgrades. Run:
52 |
53 | `npx hardhat run --network rinkeby scripts/transfer-ownership.js`
54 |
55 | Note: we're working on letting you create Safe multisigs from Defender, so stay tuned for simplifications of this step! :-)
56 |
57 | ## Creating Defender Admin Upgrade proposals from this project
58 |
59 | Take a look at `scripts/propose-upgrade.js`. The main moving parts are: the address of your contract (aka Proxy), the version of the contract you want to upgrade to, and the proposal metadata object where you can set `title` and `description` attributes to provide a high level overview of what you're trying to accomplish with this upgrade. Make sure all of those point to the right stuff and then run:
60 |
61 | `npx hardhat run --network rinkeby scripts/propose-upgrade.js`
62 |
63 | If the script succeeds, you'll get a proposal URL printed to the console. Navigate to that link and you'll see the upgrade proposal in Defender, ready to approve and/or execute.
64 |
--------------------------------------------------------------------------------
/05-upgrades-management/code/contracts/Box.sol:
--------------------------------------------------------------------------------
1 | // contracts/Box.sol
2 | // SPDX-License-Identifier: MIT
3 | pragma solidity ^0.8.0;
4 |
5 | import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
6 |
7 | contract Box is Initializable {
8 | uint256 private value;
9 |
10 | // Emitted when the stored value changes
11 | event ValueChanged(uint256 newValue);
12 |
13 | function initialize(uint256 initialValue) public initializer {
14 | value = initialValue;
15 | }
16 |
17 | // Stores a new value in the contract
18 | function store(uint256 newValue) public {
19 | value = newValue;
20 | emit ValueChanged(newValue);
21 | }
22 |
23 | // Reads the last stored value
24 | function retrieve() public view returns (uint256) {
25 | return value;
26 | }
27 |
28 | function version() public virtual pure returns (string memory) {
29 | return "1.0.0";
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/05-upgrades-management/code/contracts/BoxV2.sol:
--------------------------------------------------------------------------------
1 | // contracts/Box.sol
2 | // SPDX-License-Identifier: MIT
3 | pragma solidity ^0.8.0;
4 |
5 | import "./Box.sol";
6 |
7 | contract BoxV2 is Box {
8 | function increment() public {
9 | store(retrieve() + 1);
10 | }
11 |
12 | function version() public virtual override pure returns (string memory) {
13 | return "2.0.0";
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/05-upgrades-management/code/contracts/BoxV3.sol:
--------------------------------------------------------------------------------
1 | // contracts/Box.sol
2 | // SPDX-License-Identifier: MIT
3 | pragma solidity ^0.8.0;
4 |
5 | import "./BoxV2.sol";
6 |
7 | contract BoxV3 is BoxV2 {
8 | function decrement() public {
9 | store(retrieve() - 1);
10 | }
11 |
12 | function version() public virtual override pure returns (string memory) {
13 | return "3.0.0";
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/05-upgrades-management/code/contracts/Greeter.sol:
--------------------------------------------------------------------------------
1 | //SPDX-License-Identifier: Unlicense
2 | pragma solidity ^0.8.0;
3 |
4 | import "hardhat/console.sol";
5 |
6 |
7 | contract Greeter {
8 | string greeting;
9 |
10 | constructor(string memory _greeting) {
11 | console.log("Deploying a Greeter with greeting:", _greeting);
12 | greeting = _greeting;
13 | }
14 |
15 | function greet() public view returns (string memory) {
16 | return greeting;
17 | }
18 |
19 | function setGreeting(string memory _greeting) public {
20 | console.log("Changing greeting from '%s' to '%s'", greeting, _greeting);
21 | greeting = _greeting;
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/05-upgrades-management/code/hardhat.config.js:
--------------------------------------------------------------------------------
1 | require("dotenv").config();
2 | require("@nomiclabs/hardhat-ethers");
3 | require("@openzeppelin/hardhat-upgrades");
4 | require('@openzeppelin/hardhat-defender');
5 |
6 | const mnemonic = process.env.MNEMONIC;
7 | const infuraKey = process.env.INFURA_API_KEY;
8 |
9 | module.exports = {
10 | defender: {
11 | apiKey: process.env.DEFENDER_TEAM_API_KEY,
12 | apiSecret: process.env.DEFENDER_TEAM_API_SECRET_KEY
13 | },
14 | networks: {
15 | rinkeby: {
16 | url: `https://rinkeby.infura.io/v3/${infuraKey}`,
17 | accounts: [ mnemonic ]
18 | }
19 | },
20 | solidity: '0.8.2'
21 | };
22 |
--------------------------------------------------------------------------------
/05-upgrades-management/code/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "hardhat-project",
3 | "devDependencies": {
4 | "@nomiclabs/hardhat-ethers": "^2.0.2",
5 | "@nomiclabs/hardhat-waffle": "^2.0.1",
6 | "@openzeppelin/contracts-upgradeable": "^4.3.2",
7 | "@openzeppelin/hardhat-defender": "^1.5.1",
8 | "@openzeppelin/hardhat-upgrades": "^1.12.0",
9 | "chai": "^4.3.4",
10 | "dotenv": "^8.2.0",
11 | "ethereum-waffle": "^3.4.0",
12 | "ethers": "^5.5.1",
13 | "hardhat": "^2.6.8"
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/05-upgrades-management/code/scripts/deploy.js:
--------------------------------------------------------------------------------
1 | // scripts/deploy.js
2 | async function main () {
3 | const Box = await ethers.getContractFactory('Box');
4 | console.log('Deploying Box...');
5 | const box = await upgrades.deployProxy(Box, [42]);
6 | await box.deployed();
7 | console.log('Box deployed to:', box.address);
8 | }
9 |
10 | main()
11 | .then(() => process.exit(0))
12 | .catch(error => {
13 | console.error(error);
14 | process.exit(1);
15 | });
16 |
--------------------------------------------------------------------------------
/05-upgrades-management/code/scripts/propose-upgrade.js:
--------------------------------------------------------------------------------
1 | // scripts/propose-upgrade.js
2 | const { defender } = require("hardhat");
3 |
4 | async function main() {
5 | const proxyAddress = 'replace with your proxy address';
6 |
7 | const BoxV2 = await ethers.getContractFactory("BoxV2");
8 | console.log("Preparing proposal...");
9 | const proposal = await defender.proposeUpgrade(proxyAddress, BoxV2, { title: 'Rollback to V2' });
10 | console.log("Upgrade proposal created at:", proposal.url);
11 | }
12 |
13 | main()
14 | .then(() => process.exit(0))
15 | .catch(error => {
16 | console.error(error);
17 | process.exit(1);
18 | })
19 |
20 |
--------------------------------------------------------------------------------
/05-upgrades-management/code/scripts/sample-script.js:
--------------------------------------------------------------------------------
1 | // We require the Hardhat Runtime Environment explicitly here. This is optional
2 | // but useful for running the script in a standalone fashion through `node