diff --git a/.github/workflows/scratch-deploy-local.yml b/.github/workflows/scratch-deploy-local.yml
new file mode 100644
index 000000000..af13cbf1e
--- /dev/null
+++ b/.github/workflows/scratch-deploy-local.yml
@@ -0,0 +1,55 @@
+name: Scratch deploy on local clean anvil
+
+on:
+ pull_request:
+ branches: [master, develop]
+
+jobs:
+ do-scratch-deploy:
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ persist-credentials: false
+
+ - name: Install Foundry
+ uses: foundry-rs/foundry-toolchain@v1
+
+ - name: Start anvil in background
+ # For the port see port in scripts/scratch/dao-local-deploy.sh in RPC_URL env var
+ run: anvil -p 8555 --mnemonic "test test test test test test test test test test test junk" &
+
+ - name: Setup node.js version
+ uses: actions/setup-node@v4
+ with:
+ node-version: 18
+
+ - name: Get yarn cache directory path
+ id: yarn-cache-dir-path
+ run: echo "dir=$(yarn config get cacheFolder)" >> $GITHUB_OUTPUT
+
+ - name: Cache yarn cache
+ id: cache-yarn-cache
+ uses: actions/cache@v3
+ with:
+ path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
+ key: yarn-${{ hashFiles('**/yarn.lock') }}
+ restore-keys: yarn-${{ hashFiles('**/yarn.lock') }}
+
+ - name: Cache node_modules
+ id: cache-node-modules
+ uses: actions/cache@v3
+ with:
+ path: '**/node_modules'
+ key: node_modules-${{ hashFiles('**/yarn.lock') }}
+ restore-keys: node_modules-${{ hashFiles('**/yarn.lock') }}
+
+ - name: Install modules
+ run: yarn
+ if: |
+ steps.cache-yarn-cache.outputs.cache-hit != 'true' ||
+ steps.cache-node-modules.outputs.cache-hit != 'true'
+
+ - name: Run local deploy script
+ run: bash scripts/scratch/dao-local-deploy.sh
diff --git a/.gitignore b/.gitignore
index 488417b46..e90d847e4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -20,6 +20,7 @@ foundry/out/
# Extracted ABI files
lib/abi/*.json
+# Sensitive data
.env
accounts.json
deployed-local.json
diff --git a/apps/simple-dvt/README.md b/apps/simple-dvt/README.md
new file mode 100644
index 000000000..994a366dc
--- /dev/null
+++ b/apps/simple-dvt/README.md
@@ -0,0 +1,80 @@
+# StakingRouter Aragon App
+
+This directory contains source files for the [StakingRouter Aragon frontend app](https://mainnet.lido.fi/#/lido-dao/0x55032650b14df07b85bf18a3a3ec8e0af2e028d5/).
+
+## Verifying source code
+
+To verify that the StakingRouter app frontend was built from this source code, please follow instructions below.
+
+### Prerequisites
+
+- git
+- Node.js 16.14.2
+- ipfs 0.19.0
+
+### 1. Replicating IPFS hash and content URI
+
+Clone the Lido DAO repo,
+
+```bash
+git clone https://github.com/lidofinance/lido-dao.git
+```
+
+Go into the directory,
+
+```bash
+cd lido-dao
+```
+
+Checkout [this commit](https://github.com/lidofinance/lido-dao/commit/34f5d0d428fcb51aae74f0cb7387b9bd59916817) (the latest `yarn.lock` update for the StakingRouter app),
+
+```bash
+git checkout 34f5d0d428fcb51aae74f0cb7387b9bd59916817
+```
+
+Install dependencies **without updating the lockfile**. This will make sure that you're using the same versions of the dependencies that were used to develop the app,
+
+```bash
+yarn install --immutable
+```
+
+Build the static assets for the app,
+
+```bash
+# legacy app name
+export APPS=simple-dvt
+npx hardhat run scripts/build-apps-frontend.js
+```
+
+Get the IPFS hash of the build folder,
+
+```bash
+ipfs add -qr --only-hash apps/simple-dvt/dist/ | tail -n 1
+```
+
+This command should output `QmaSSujHCGcnFuetAPGwVW5BegaMBvn5SCsgi3LSfvraSo`.
+
+Now we have to obtain the content URI, which is this hash encoded for Aragon.
+
+Now we run the script,
+
+```bash
+export IPFS_HASH=QmaSSujHCGcnFuetAPGwVW5BegaMBvn5SCsgi3LSfvraSo
+npx hardhat run scripts/helpers/getContentUri.js
+```
+
+This command should print `0x697066733a516d54346a64693146684d454b5576575351316877786e33365748394b6a656743755a7441684a6b6368526b7a70`, which is our content URI.
+
+### 2. Verifying on-chain StakingRouter App content URI
+
+Open the [NodeOperatorsRegistry App Repo](https://etherscan.io/address/0x0D97E876ad14DB2b183CFeEB8aa1A5C788eB1831#readProxyContract) and scroll down to `getLatest` method, open the dropdown and click "Query". This will give you the NodeOperatorsRegistry app version, contract address and the content URI. Now check that the content URI that you've obtained in the previous step matches the one that Etherscan fetched for you from the contract.
+
+### 3. Verifying client-side resources
+
+Now that we have the IPFS hash and content URI, let's see that it is, in fact, the one that's used on the DAO website.
+
+Open the [StakingRouter app](https://mainnet.lido.fi/#/lido-dao/0x55032650b14df07b85bf18a3a3ec8e0af2e028d5/) in your browser, then open the network inspector and refresh the page to track all of the network requests that the website makes.
+
+You will find that one of the two HTML files has, in fact, been loaded from `https://ipfs.mainnet.fi/ipfs/QmaSSujHCGcnFuetAPGwVW5BegaMBvn5SCsgi3LSfvraSo/index.html`.
+
+You are done! ✨
diff --git a/apps/simple-dvt/app/.babelrc b/apps/simple-dvt/app/.babelrc
new file mode 100644
index 000000000..13d2b95a1
--- /dev/null
+++ b/apps/simple-dvt/app/.babelrc
@@ -0,0 +1,30 @@
+{
+ "presets": [
+ [
+ "@babel/preset-env",
+ {
+ "modules": false,
+ "targets": {
+ "browsers": [
+ "> 1%",
+ "last 3 versions",
+ "ie >= 9",
+ "ios >= 8",
+ "android >= 4.2"
+ ]
+ },
+ "useBuiltIns": "entry",
+ "corejs": 3,
+ "shippedProposals": true,
+ }
+ ]
+ ],
+ "plugins": [
+ [
+ "styled-components",
+ {
+ "displayName": true
+ }
+ ]
+ ]
+}
diff --git a/apps/simple-dvt/app/.eslintrc b/apps/simple-dvt/app/.eslintrc
new file mode 100644
index 000000000..0f19e1dc6
--- /dev/null
+++ b/apps/simple-dvt/app/.eslintrc
@@ -0,0 +1,21 @@
+{
+ "env": {
+ "browser": true,
+ "es6": true
+ },
+ "extends": [
+ "standard",
+ "standard-react",
+ "plugin:prettier/recommended",
+ "prettier/react"
+ ],
+ "parser": "babel-eslint",
+ "plugins": ["prettier", "react", "react-hooks"],
+ "rules": {
+ "valid-jsdoc": "error",
+ "react/prop-types": 0,
+ "linebreak-style": ["error", "unix"],
+ "react-hooks/rules-of-hooks": "error",
+ "react-hooks/exhaustive-deps": "warn"
+ }
+}
diff --git a/apps/simple-dvt/app/.gitignore b/apps/simple-dvt/app/.gitignore
new file mode 100644
index 000000000..383b8ed65
--- /dev/null
+++ b/apps/simple-dvt/app/.gitignore
@@ -0,0 +1,31 @@
+# See https://help.github.com/ignore-files/ for more about ignoring files.
+
+# cache
+.cache
+
+# dependencies
+/node_modules
+
+# testing
+/coverage
+
+# production
+/build
+/dist
+
+# misc
+.env
+.DS_Store
+.env.local
+.env.development.local
+.env.test.local
+.env.production.local
+
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# built assets
+/public/aragon-ui
+/public/script.js
+/public/script.map
diff --git a/apps/simple-dvt/app/.prettierrc b/apps/simple-dvt/app/.prettierrc
new file mode 100644
index 000000000..5824bbabb
--- /dev/null
+++ b/apps/simple-dvt/app/.prettierrc
@@ -0,0 +1,7 @@
+{
+ "singleQuote": true,
+ "semi": false,
+ "trailingComma": "es5",
+ "bracketSpacing": true,
+ "jsxBracketSameLine": false
+}
diff --git a/apps/simple-dvt/app/index.html b/apps/simple-dvt/app/index.html
new file mode 100644
index 000000000..07f0586fb
--- /dev/null
+++ b/apps/simple-dvt/app/index.html
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+ Aragon App
+
+
+
+ You need to enable JavaScript to run this app.
+
+
+
+
+
diff --git a/apps/simple-dvt/app/package.json b/apps/simple-dvt/app/package.json
new file mode 100644
index 000000000..ddc3692cc
--- /dev/null
+++ b/apps/simple-dvt/app/package.json
@@ -0,0 +1,50 @@
+{
+ "name": "simple-dvt-frontend",
+ "version": "1.0.0",
+ "main": "src/index.js",
+ "dependencies": {
+ "@aragon/api": "^2.0.0",
+ "@aragon/api-react": "^2.0.0",
+ "@aragon/ui": "^1.7.0",
+ "core-js": "^3.6.5",
+ "formik": "^2.2.0",
+ "react": "^16.13.1",
+ "react-dom": "^16.13.1",
+ "regenerator-runtime": "^0.13.7",
+ "styled-components": "^5.2.0",
+ "yup": "^0.29.3"
+ },
+ "devDependencies": {
+ "@babel/core": "^7.21.0",
+ "@babel/preset-env": "^7.11.5",
+ "@babel/preset-react": "^7.10.1",
+ "babel-eslint": "^10.1.0",
+ "babel-plugin-styled-components": "^1.11.1",
+ "copyfiles": "^2.3.0",
+ "eslint": "^8.34.0",
+ "eslint-config-prettier": "^8.6.0",
+ "eslint-config-standard": "^17.0.0",
+ "eslint-config-standard-react": "^9.2.0",
+ "eslint-plugin-import": "^2.27.5",
+ "eslint-plugin-node": "^11.1.0",
+ "eslint-plugin-prettier": "^4.2.1",
+ "eslint-plugin-promise": "^6.1.1",
+ "eslint-plugin-react": "^7.20.6",
+ "eslint-plugin-react-hooks": "^4.1.2",
+ "eslint-plugin-standard": "^5.0.0",
+ "parcel-bundler": "^1.12.4",
+ "prettier": "^2.8.4"
+ },
+ "scripts": {
+ "build": "yarn sync-assets && yarn build:app && yarn build:script",
+ "build:app": "parcel build index.html -d ../dist/ --public-url \".\" --no-cache",
+ "build:script": "parcel build src/script.js --out-dir ../dist/ --no-cache",
+ "watch:script": "parcel watch src/script.js --out-dir ../dist/ --no-hmr",
+ "serve": "parcel serve index.html --out-dir ../dist/ --no-cache",
+ "watch": "yarn watch:script",
+ "sync-assets": "copy-aragon-ui-assets ../dist && copyfiles -u 1 './public/**/*' ../dist",
+ "start": "yarn sync-assets && yarn watch:script & yarn serve",
+ "dev": "yarn sync-assets && yarn watch:script & yarn serve -- --port 3012",
+ "dev-fallback": "bash -c 'yarn sync-assets && yarn watch:script & yarn serve --port 3012'"
+ }
+}
diff --git a/apps/simple-dvt/app/public/meta/details.md b/apps/simple-dvt/app/public/meta/details.md
new file mode 100644
index 000000000..d62d03083
--- /dev/null
+++ b/apps/simple-dvt/app/public/meta/details.md
@@ -0,0 +1,7 @@
+An application for Aragon.
+
+**Features**
+
+- Feature \#1.
+- Feature \#2.
+- Feature \#3.
diff --git a/apps/simple-dvt/app/public/meta/icon.svg b/apps/simple-dvt/app/public/meta/icon.svg
new file mode 100644
index 000000000..546d85afe
--- /dev/null
+++ b/apps/simple-dvt/app/public/meta/icon.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/apps/simple-dvt/app/public/meta/screenshot-1.png b/apps/simple-dvt/app/public/meta/screenshot-1.png
new file mode 100644
index 000000000..b7f817650
Binary files /dev/null and b/apps/simple-dvt/app/public/meta/screenshot-1.png differ
diff --git a/apps/simple-dvt/app/sample.env b/apps/simple-dvt/app/sample.env
new file mode 100644
index 000000000..a756aa37d
--- /dev/null
+++ b/apps/simple-dvt/app/sample.env
@@ -0,0 +1,3 @@
+SK_LIMIT=20
+SUBGRAPH_ENDPOINT=https://holesky.lido.fi/key-checker/api/subgraph
+SIGNATURE_VERIFY_ENDPOINT=https://holesky.lido.fi/key-checker/api/signature
diff --git a/apps/simple-dvt/app/src/App.js b/apps/simple-dvt/app/src/App.js
new file mode 100644
index 000000000..b8132766f
--- /dev/null
+++ b/apps/simple-dvt/app/src/App.js
@@ -0,0 +1,36 @@
+import React from 'react'
+import { useAragonApi, useGuiStyle } from '@aragon/api-react'
+import {
+ Button,
+ Header,
+ Main,
+ Split,
+ SyncIndicator,
+ useTheme,
+} from '@aragon/ui'
+import { ThemeProvider } from 'styled-components'
+import { SimpleDVTPrimary, SimpleDVTSecondary } from './components/SimpleDVT'
+
+const App = () => {
+ const { appState } = useAragonApi()
+ const { appearance } = useGuiStyle()
+ const { isSyncing } = appState
+
+ console.log(appState)
+
+ const theme = useTheme()
+
+ return (
+
+
+
+
+ } secondary={ } />
+
+
+ )
+}
+
+export default App
diff --git a/apps/simple-dvt/app/src/components/AddNodeOperatorSidePanel.js b/apps/simple-dvt/app/src/components/AddNodeOperatorSidePanel.js
new file mode 100644
index 000000000..f705fba8b
--- /dev/null
+++ b/apps/simple-dvt/app/src/components/AddNodeOperatorSidePanel.js
@@ -0,0 +1,76 @@
+import { Button, GU, SidePanel } from '@aragon/ui'
+import React from 'react'
+import { Formik, Field } from 'formik'
+import * as yup from 'yup'
+import TextField from './TextField'
+
+const initialValues = {
+ name: '',
+ address: '',
+}
+
+const validationSchema = yup.object().shape({
+ name: yup.string().required().min(1),
+ address: yup.string().required().min(1),
+})
+
+function PanelContent({ addNodeOperatorApi, onClose }) {
+ const onSubmit = ({ name, address }) => {
+ addNodeOperatorApi(name, address)
+ .catch(console.error)
+ .finally(() => {
+ onClose()
+ })
+ }
+
+ return (
+
+ {({ submitForm, isSubmitting, errors, values }) => {
+ return (
+
+ )
+ }}
+
+ )
+}
+
+export default (props) => (
+
+
+
+)
diff --git a/apps/simple-dvt/app/src/components/AddSigningKeysSidePanel.js b/apps/simple-dvt/app/src/components/AddSigningKeysSidePanel.js
new file mode 100644
index 000000000..4c5fcec05
--- /dev/null
+++ b/apps/simple-dvt/app/src/components/AddSigningKeysSidePanel.js
@@ -0,0 +1,185 @@
+import { Button, GU, SidePanel, Info, SyncIndicator } from '@aragon/ui'
+import React, { useCallback } from 'react'
+import { Formik, Field } from 'formik'
+import * as yup from 'yup'
+import TextField from './TextField'
+import {
+ checkForDuplicatesAsync,
+ formatJsonData,
+ hasDuplicatePubkeys,
+ hasDuplicateSigs,
+ isHexadecimal,
+ SIGNATURE_VERIFY_ENDPOINT,
+ SUBGRAPH_ENDPOINT,
+ verifySignaturesAsync,
+} from '../utils/helpers'
+import CheckBox from './CheckBox'
+
+const DEFAULT_LIMIT = 20
+const LIMIT = process.env.SK_LIMIT || DEFAULT_LIMIT
+
+const initialValues = {
+ json: '',
+ useAdvancedV8n: false,
+}
+
+const validationSchema = yup
+ .object()
+ .shape({
+ json: yup.string().required(),
+ })
+ .test('basic', 'Invalid json file', function ({ json }) {
+ let data
+ try {
+ data = JSON.parse(json)
+ if (!Array.isArray(data)) {
+ throw new Error('JSON must be an array')
+ }
+ } catch (e) {
+ return this.createError({
+ path: 'json',
+ message: e.message || 'Invalid JSON',
+ })
+ }
+
+ const quantity = data.length
+ if (quantity < 1)
+ return this.createError({
+ path: 'json',
+ message: `Expected one or more keys but got ${quantity}.`,
+ })
+
+ if (quantity > LIMIT)
+ return this.createError({
+ path: 'json',
+ message: `Expected ${LIMIT} signing keys max per submission but got ${quantity}.`,
+ })
+
+ if (hasDuplicatePubkeys(data))
+ return this.createError({
+ path: 'json',
+ message: 'Includes duplicate public keys',
+ })
+
+ if (hasDuplicateSigs(data))
+ return this.createError({
+ path: 'json',
+ message: 'Includes duplicate signatures',
+ })
+
+ for (let i = 0; i < data.length; i++) {
+ const { pubkey, signature } = data[i]
+
+ if (!isHexadecimal(pubkey, 96))
+ return this.createError({
+ path: 'json',
+ message: `Invalid pubkey at index ${i}.`,
+ })
+ if (!isHexadecimal(signature, 192))
+ return this.createError({
+ path: 'json',
+ message: `Invalid signature at index ${i}.`,
+ })
+ }
+
+ return true
+ })
+ .test('advanced', 'Invalid keys', async function ({ json, useAdvancedV8n }) {
+ if (!useAdvancedV8n) return true
+
+ const signingKeys = JSON.parse(json)
+
+ const duplicates = await checkForDuplicatesAsync(signingKeys)
+ if (duplicates.length) {
+ return this.createError({
+ path: 'json',
+ message: `Public keys already in use: ${duplicates.join(', ')}`,
+ })
+ }
+
+ const invalidSignatures = await verifySignaturesAsync(signingKeys)
+ if (invalidSignatures.length) {
+ return this.createError({
+ path: 'json',
+ message: `Invalid signatures: ${invalidSignatures.join(', ')}`,
+ })
+ }
+
+ return true
+ })
+
+function PanelContent({ api, onClose }) {
+ const onSubmit = useCallback(
+ async ({ json }) => {
+ const { quantity, pubkeys, signatures } = formatJsonData(json)
+
+ api(quantity, pubkeys, signatures)
+ .catch(console.error)
+ .then(() => {
+ onClose()
+ })
+ },
+ [api, onClose]
+ )
+
+ return (
+
+ {({ submitForm, isSubmitting, isValidating }) => {
+ return (
+
+ )
+ }}
+
+ )
+}
+
+export default (props) => (
+
+
+
+)
diff --git a/apps/simple-dvt/app/src/components/ChangeLimitPanel.js b/apps/simple-dvt/app/src/components/ChangeLimitPanel.js
new file mode 100644
index 000000000..289493c20
--- /dev/null
+++ b/apps/simple-dvt/app/src/components/ChangeLimitPanel.js
@@ -0,0 +1,81 @@
+import { Button, GU, SidePanel, Info } from '@aragon/ui'
+import React, { useCallback } from 'react'
+import { Formik, Field } from 'formik'
+import * as yup from 'yup'
+import TextField from './TextField'
+
+const initialValues = {
+ limit: 0,
+}
+
+const validationSchema = yup.object().shape({
+ limit: yup.number().positive().integer().required().min(0),
+})
+
+function PanelContent({ api, onClose }) {
+ const onSubmit = useCallback(
+ ({ limit }) => {
+ api(limit)
+ .catch(console.error)
+ .then(() => {
+ onClose()
+ })
+ },
+ [api, onClose]
+ )
+
+ return (
+
+ {({ submitForm, isSubmitting, isValidating }) => {
+ return (
+
+ )
+ }}
+
+ )
+}
+
+export default (props) => (
+
+
+
+)
diff --git a/apps/simple-dvt/app/src/components/CheckBox.js b/apps/simple-dvt/app/src/components/CheckBox.js
new file mode 100644
index 000000000..9f7aad09e
--- /dev/null
+++ b/apps/simple-dvt/app/src/components/CheckBox.js
@@ -0,0 +1,44 @@
+import { Info, Checkbox as AragonCheckbox, GU } from '@aragon/ui'
+import React, { useCallback } from 'react'
+
+const CheckBox = React.forwardRef(({ label, field, form }, ref) => {
+ const handleChange = useCallback(
+ (checked) => {
+ form.setFieldValue('useAdvancedV8n', checked)
+ },
+ [form]
+ )
+
+ return (
+
+
+
+ Use advanced validation
+
+
+ By checking this box, you agree to using an external api to check your
+ signing keys for duplicates against already submitted keys and to verify
+ your signatures.
+
+
+ )
+})
+
+export default CheckBox
diff --git a/apps/simple-dvt/app/src/components/InfoBox.js b/apps/simple-dvt/app/src/components/InfoBox.js
new file mode 100644
index 000000000..ede4d4d9f
--- /dev/null
+++ b/apps/simple-dvt/app/src/components/InfoBox.js
@@ -0,0 +1,17 @@
+import { Box } from '@aragon/ui'
+import React from 'react'
+
+export default function InfoBox({ heading, value }) {
+ return (
+
+
+ {value}
+
+
+ )
+}
diff --git a/apps/simple-dvt/app/src/components/ListItem.js b/apps/simple-dvt/app/src/components/ListItem.js
new file mode 100644
index 000000000..facd2f490
--- /dev/null
+++ b/apps/simple-dvt/app/src/components/ListItem.js
@@ -0,0 +1,41 @@
+import { GU, useTheme } from '@aragon/ui'
+import React from 'react'
+import styled from 'styled-components'
+
+const ListItemStyle = styled.li`
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: ${GU}px ${GU * 3}px ${GU}px
+ ${(props) => (props.nested ? GU * 6 : GU * 3)}px;
+ line-height: 40px;
+ border-top: 1px solid
+ ${(props) => (props.isDark ? '#2C3A58' : props.theme.border)};
+
+ & :first-of-type {
+ margin-top: 0;
+ border-top: none;
+ }
+`
+
+const ListItemLabel = styled.span`
+ color: ${(props) =>
+ props.isDark ? '#7C99D6' : props.theme.surfaceContentSecondary};
+`
+
+const ListItemValue = styled.strong`
+ text-align: right;
+`
+
+export const ListItem = ({ label, children, nested }) => {
+ const theme = useTheme()
+
+ const themeDark = theme?._name === 'dark'
+
+ return (
+
+ {label}
+ {children}
+
+ )
+}
diff --git a/apps/simple-dvt/app/src/components/ListItemAddress.js b/apps/simple-dvt/app/src/components/ListItemAddress.js
new file mode 100644
index 000000000..b378db147
--- /dev/null
+++ b/apps/simple-dvt/app/src/components/ListItemAddress.js
@@ -0,0 +1,14 @@
+import React from 'react'
+import { IdentityBadge } from '@aragon/ui'
+import { ListItem } from './ListItem'
+import { LoadableElement } from './LoadableElement'
+
+export const ListItemAddress = ({ label, value }) => {
+ return (
+
+
+
+
+
+ )
+}
diff --git a/apps/simple-dvt/app/src/components/ListItemBoolean.js b/apps/simple-dvt/app/src/components/ListItemBoolean.js
new file mode 100644
index 000000000..45bfebccc
--- /dev/null
+++ b/apps/simple-dvt/app/src/components/ListItemBoolean.js
@@ -0,0 +1,11 @@
+import React from 'react'
+import { ListItem } from './ListItem'
+import { LoadableElement } from './LoadableElement'
+
+export const ListItemBoolean = ({ label, value }) => {
+ return (
+
+ {value ? 'Yes' : 'No'}
+
+ )
+}
diff --git a/apps/simple-dvt/app/src/components/ListItemUnformattedValue.js b/apps/simple-dvt/app/src/components/ListItemUnformattedValue.js
new file mode 100644
index 000000000..9afaa736b
--- /dev/null
+++ b/apps/simple-dvt/app/src/components/ListItemUnformattedValue.js
@@ -0,0 +1,11 @@
+import React from 'react'
+import { ListItem } from './ListItem'
+import { LoadableElement } from './LoadableElement'
+
+export const ListItemUnformattedValue = ({ label, value }) => {
+ return (
+
+ {value}
+
+ )
+}
diff --git a/apps/simple-dvt/app/src/components/LoadableElement.js b/apps/simple-dvt/app/src/components/LoadableElement.js
new file mode 100644
index 000000000..a61ccfd18
--- /dev/null
+++ b/apps/simple-dvt/app/src/components/LoadableElement.js
@@ -0,0 +1,10 @@
+import React from 'react'
+import { LoadingRing } from '@aragon/ui'
+
+export const LoadableElement = ({ value, children }) => {
+ if (typeof value === 'undefined') {
+ return
+ }
+
+ return {children}
+}
diff --git a/apps/simple-dvt/app/src/components/MenuItem.js b/apps/simple-dvt/app/src/components/MenuItem.js
new file mode 100644
index 000000000..9b246ba8d
--- /dev/null
+++ b/apps/simple-dvt/app/src/components/MenuItem.js
@@ -0,0 +1,33 @@
+import { ContextMenuItem, GU, useTheme } from '@aragon/ui'
+import React from 'react'
+
+export default function MenuItem({ onClick, icon, label }) {
+ const theme = useTheme()
+
+ return (
+
+
+ {icon}
+
+
+ {label}
+
+
+ )
+}
diff --git a/apps/simple-dvt/app/src/components/NodeOperatorList.js b/apps/simple-dvt/app/src/components/NodeOperatorList.js
new file mode 100644
index 000000000..23bb2bb3c
--- /dev/null
+++ b/apps/simple-dvt/app/src/components/NodeOperatorList.js
@@ -0,0 +1,66 @@
+import { useAppState } from '@aragon/api-react'
+import { DataView, GU, Help, IdentityBadge } from '@aragon/ui'
+import React from 'react'
+import styled, { keyframes } from 'styled-components'
+
+const ColumnName = styled.div`
+ display: flex;
+ justify-content: flex-end;
+ & > :first-child {
+ margin-right: ${GU}px;
+ }
+`
+
+const blink = keyframes`
+ from {
+ opacity: 1;
+ }
+ to {
+ opacity: 0;
+ }
+`
+
+const Dot = styled.span`
+ margin-right: ${GU}px;
+ color: ${(props) =>
+ props.active ? props.theme.positive : props.theme.negative};
+ animation: ${blink} 0.5s infinite alternate;
+`
+
+export const NodeOperatorList = () => {
+ let { nodeOperators } = useAppState()
+
+ nodeOperators = nodeOperators || []
+
+ return (
+
+ SL / SV / SKu / SKt
{' '}
+
+ Staking limit / Stopped validators / Used signing keys / Total
+ signing keys{' '}
+
+ ,
+ ]}
+ entries={nodeOperators}
+ renderEntry={(no) => [
+
+ •
+ ,
+ no.name,
+ ,
+ no.stakingLimit +
+ '/' +
+ no.stoppedValidators +
+ '/' +
+ no.usedSigningKeys +
+ '/' +
+ no.totalSigningKeys,
+ ]}
+ />
+ )
+}
diff --git a/apps/simple-dvt/app/src/components/Primary.js b/apps/simple-dvt/app/src/components/Primary.js
new file mode 100644
index 000000000..a37106be7
--- /dev/null
+++ b/apps/simple-dvt/app/src/components/Primary.js
@@ -0,0 +1,48 @@
+import { useAppState } from '@aragon/api-react'
+import React from 'react'
+import { ListItemUnformattedValue } from './ListItemUnformattedValue'
+import { NodeOperatorList } from './NodeOperatorList'
+import { BoxUnpadded } from './styles'
+
+export const Primary = () => {
+ const {
+ nonce,
+ nodeOperatorsCount,
+ activeNodeOperatorsCount,
+ stakingModuleSummary,
+ stuckPenaltyDelay,
+ } = useAppState()
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+ >
+ )
+}
diff --git a/apps/simple-dvt/app/src/components/Secondary.js b/apps/simple-dvt/app/src/components/Secondary.js
new file mode 100644
index 000000000..6d3be7171
--- /dev/null
+++ b/apps/simple-dvt/app/src/components/Secondary.js
@@ -0,0 +1,31 @@
+import React from 'react'
+import { BoxUnpadded } from './styles'
+import { ListItemUnformattedValue } from './ListItemUnformattedValue'
+import { useAppState } from '@aragon/api-react'
+import { ListItemBoolean } from './ListItemBoolean'
+
+export const Secondary = () => {
+ const {
+ stakingModuleType,
+ hasInitialized,
+ initializationBlock,
+ contractVersion,
+ } = useAppState()
+
+ return (
+ <>
+
+
+
+
+
+
+ >
+ )
+}
diff --git a/apps/simple-dvt/app/src/components/TextField.js b/apps/simple-dvt/app/src/components/TextField.js
new file mode 100644
index 000000000..7211d3697
--- /dev/null
+++ b/apps/simple-dvt/app/src/components/TextField.js
@@ -0,0 +1,17 @@
+import { Field, Info, TextInput } from '@aragon/ui'
+import React from 'react'
+
+const TextField = React.forwardRef(({ label, field, form, ...props }, ref) => {
+ return (
+
+
+ {form.errors[field.name] && (
+
+ {form.errors[field.name]}
+
+ )}
+
+ )
+})
+
+export default TextField
diff --git a/apps/simple-dvt/app/src/components/shared/BasisPoints.js b/apps/simple-dvt/app/src/components/shared/BasisPoints.js
new file mode 100644
index 000000000..e76f3c129
--- /dev/null
+++ b/apps/simple-dvt/app/src/components/shared/BasisPoints.js
@@ -0,0 +1,15 @@
+import React from 'react'
+import { LoadingRing } from '@aragon/ui'
+
+export const BasisPoints = ({ basisPoints }) => {
+ if (typeof basisPoints === 'undefined' || Number.isNaN(basisPoints)) {
+ return
+ }
+
+ basisPoints = Number(basisPoints)
+ if (Number.isNaN(basisPoints)) {
+ return N/A
+ }
+
+ return {basisPoints / 100}%
+}
diff --git a/apps/simple-dvt/app/src/components/shared/BytesBadge.js b/apps/simple-dvt/app/src/components/shared/BytesBadge.js
new file mode 100644
index 000000000..5a9b50b0f
--- /dev/null
+++ b/apps/simple-dvt/app/src/components/shared/BytesBadge.js
@@ -0,0 +1,44 @@
+import { GU, textStyle } from '@aragon/ui'
+import BadgeBase from '@aragon/ui/dist/BadgeBase'
+import IconCheck from '@aragon/ui/dist/IconCheck'
+import React, { useCallback, useEffect, useState } from 'react'
+import { CopyToClipboard } from 'react-copy-to-clipboard'
+import styled from 'styled-components'
+
+const BadgeText = styled.span`
+ margin-left: ${0.5 * GU}px;
+`
+
+export const BytesBadge = ({ bytes }) => {
+ const shortened =
+ typeof bytes === 'string'
+ ? `${bytes.substring(0, 6)}…${bytes.substring(60)}`
+ : ''
+
+ const [copied, setCopied] = useState(false)
+ const handleCopy = useCallback(() => setCopied(true), [])
+
+ useEffect(() => {
+ let interval
+ if (copied) {
+ interval = setInterval(() => {
+ setCopied(false)
+ }, 3000)
+ }
+
+ return () => clearInterval(interval)
+ }, [copied])
+
+ return (
+
+
+ {copied && }
+ {shortened}
+ >
+ }
+ />
+
+ )
+}
diff --git a/apps/simple-dvt/app/src/components/shared/ListItem.js b/apps/simple-dvt/app/src/components/shared/ListItem.js
new file mode 100644
index 000000000..facd2f490
--- /dev/null
+++ b/apps/simple-dvt/app/src/components/shared/ListItem.js
@@ -0,0 +1,41 @@
+import { GU, useTheme } from '@aragon/ui'
+import React from 'react'
+import styled from 'styled-components'
+
+const ListItemStyle = styled.li`
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: ${GU}px ${GU * 3}px ${GU}px
+ ${(props) => (props.nested ? GU * 6 : GU * 3)}px;
+ line-height: 40px;
+ border-top: 1px solid
+ ${(props) => (props.isDark ? '#2C3A58' : props.theme.border)};
+
+ & :first-of-type {
+ margin-top: 0;
+ border-top: none;
+ }
+`
+
+const ListItemLabel = styled.span`
+ color: ${(props) =>
+ props.isDark ? '#7C99D6' : props.theme.surfaceContentSecondary};
+`
+
+const ListItemValue = styled.strong`
+ text-align: right;
+`
+
+export const ListItem = ({ label, children, nested }) => {
+ const theme = useTheme()
+
+ const themeDark = theme?._name === 'dark'
+
+ return (
+
+ {label}
+ {children}
+
+ )
+}
diff --git a/apps/simple-dvt/app/src/components/shared/ListItemAddress.js b/apps/simple-dvt/app/src/components/shared/ListItemAddress.js
new file mode 100644
index 000000000..b378db147
--- /dev/null
+++ b/apps/simple-dvt/app/src/components/shared/ListItemAddress.js
@@ -0,0 +1,14 @@
+import React from 'react'
+import { IdentityBadge } from '@aragon/ui'
+import { ListItem } from './ListItem'
+import { LoadableElement } from './LoadableElement'
+
+export const ListItemAddress = ({ label, value }) => {
+ return (
+
+
+
+
+
+ )
+}
diff --git a/apps/simple-dvt/app/src/components/shared/ListItemBasisPoints.js b/apps/simple-dvt/app/src/components/shared/ListItemBasisPoints.js
new file mode 100644
index 000000000..e33e4aea3
--- /dev/null
+++ b/apps/simple-dvt/app/src/components/shared/ListItemBasisPoints.js
@@ -0,0 +1,14 @@
+import React from 'react'
+import { BasisPoints } from './BasisPoints'
+import { ListItem } from './ListItem'
+import { LoadableElement } from './LoadableElement'
+
+export const ListItemBasisPoints = ({ label, value, ...rest }) => {
+ return (
+
+
+
+
+
+ )
+}
diff --git a/apps/simple-dvt/app/src/components/shared/ListItemBoolean.js b/apps/simple-dvt/app/src/components/shared/ListItemBoolean.js
new file mode 100644
index 000000000..be4803efe
--- /dev/null
+++ b/apps/simple-dvt/app/src/components/shared/ListItemBoolean.js
@@ -0,0 +1,11 @@
+import React from 'react'
+import { ListItem } from './ListItem'
+import { LoadableElement } from './LoadableElement'
+
+export const ListItemBoolean = ({ label, value, renderElements = ["Yes", "No"] }) => {
+ return (
+
+ {value ? renderElements[0] : renderElements[1]}
+
+ )
+}
diff --git a/apps/simple-dvt/app/src/components/shared/ListItemBytes.js b/apps/simple-dvt/app/src/components/shared/ListItemBytes.js
new file mode 100644
index 000000000..f2c457a1d
--- /dev/null
+++ b/apps/simple-dvt/app/src/components/shared/ListItemBytes.js
@@ -0,0 +1,15 @@
+import React from 'react'
+import { IdentityBadge } from '@aragon/ui'
+import { ListItem } from './ListItem'
+import { LoadableElement } from './LoadableElement'
+import { BytesBadge } from './BytesBadge'
+
+export const ListItemBytes = ({ label, value }) => {
+ return (
+
+
+
+
+
+ )
+}
diff --git a/apps/simple-dvt/app/src/components/shared/ListItemUnformattedValue.js b/apps/simple-dvt/app/src/components/shared/ListItemUnformattedValue.js
new file mode 100644
index 000000000..9afaa736b
--- /dev/null
+++ b/apps/simple-dvt/app/src/components/shared/ListItemUnformattedValue.js
@@ -0,0 +1,11 @@
+import React from 'react'
+import { ListItem } from './ListItem'
+import { LoadableElement } from './LoadableElement'
+
+export const ListItemUnformattedValue = ({ label, value }) => {
+ return (
+
+ {value}
+
+ )
+}
diff --git a/apps/simple-dvt/app/src/components/shared/LoadableElement.js b/apps/simple-dvt/app/src/components/shared/LoadableElement.js
new file mode 100644
index 000000000..a61ccfd18
--- /dev/null
+++ b/apps/simple-dvt/app/src/components/shared/LoadableElement.js
@@ -0,0 +1,10 @@
+import React from 'react'
+import { LoadingRing } from '@aragon/ui'
+
+export const LoadableElement = ({ value, children }) => {
+ if (typeof value === 'undefined') {
+ return
+ }
+
+ return {children}
+}
diff --git a/apps/simple-dvt/app/src/components/shared/NodeOperatorList.js b/apps/simple-dvt/app/src/components/shared/NodeOperatorList.js
new file mode 100644
index 000000000..f57dd9010
--- /dev/null
+++ b/apps/simple-dvt/app/src/components/shared/NodeOperatorList.js
@@ -0,0 +1,65 @@
+import { useAppState } from '@aragon/api-react'
+import { DataView, GU, Help, IdentityBadge } from '@aragon/ui'
+import React from 'react'
+import styled, { keyframes } from 'styled-components'
+
+const ColumnName = styled.div`
+ display: flex;
+ justify-content: flex-end;
+ & > :first-child {
+ margin-right: ${GU}px;
+ }
+`
+
+const blink = keyframes`
+ from {
+ opacity: 1;
+ }
+ to {
+ opacity: 0;
+ }
+`
+
+const Dot = styled.span`
+ margin-right: ${GU}px;
+ color: ${(props) =>
+ props.active ? props.theme.positive : props.theme.negative};
+ animation: ${blink} 0.5s infinite alternate;
+`
+
+export const NodeOperatorList = () => {
+ const { simpleDVT } = useAppState()
+
+ const nodeOperators = simpleDVT?.nodeOperators || []
+
+ return (
+
+ A / D / V / E
{' '}
+
+ Added / Deposited / Vetted / Exited{' '}
+
+ ,
+ ]}
+ entries={nodeOperators}
+ renderEntry={(no) => [
+
+ •
+ ,
+ no.name,
+ ,
+ no.totalAddedValidators +
+ '/' +
+ no.totalDepositedValidators +
+ '/' +
+ no.totalVettedValidators +
+ '/' +
+ no.totalExitedValidators,
+ ]}
+ />
+ )
+}
diff --git a/apps/simple-dvt/app/src/components/shared/Tooltip.js b/apps/simple-dvt/app/src/components/shared/Tooltip.js
new file mode 100644
index 000000000..da33dede8
--- /dev/null
+++ b/apps/simple-dvt/app/src/components/shared/Tooltip.js
@@ -0,0 +1,22 @@
+import { Help } from '@aragon/ui'
+import React from 'react'
+import styled from 'styled-components'
+
+const TooltipStyle = styled.div`
+ display: flex;
+ justify-content: flex-start;
+ align-items: center;
+`
+
+const TooltipLabel = styled.span`
+ margin-right: 8px;
+`
+
+export const Tooltip = ({ tooltip, children }) => {
+ return (
+
+ {children}
+ {tooltip && {tooltip} }
+
+ )
+}
diff --git a/apps/simple-dvt/app/src/components/shared/index.js b/apps/simple-dvt/app/src/components/shared/index.js
new file mode 100644
index 000000000..dd9b3fd40
--- /dev/null
+++ b/apps/simple-dvt/app/src/components/shared/index.js
@@ -0,0 +1,11 @@
+export * from "./styles"
+export { ListItem } from "./ListItem"
+export { LoadableElement } from "./LoadableElement"
+export { ListItemUnformattedValue } from "./ListItemUnformattedValue"
+export { ListItemAddress } from "./ListItemAddress"
+export { ListItemBytes } from "./ListItemBytes"
+export { BasisPoints } from "./BasisPoints"
+export { Tooltip } from "./Tooltip"
+export { ListItemBasisPoints } from "./ListItemBasisPoints"
+export { NodeOperatorList } from "./NodeOperatorList"
+export { ListItemBoolean } from "./ListItemBoolean"
\ No newline at end of file
diff --git a/apps/simple-dvt/app/src/components/shared/styles.js b/apps/simple-dvt/app/src/components/shared/styles.js
new file mode 100644
index 000000000..a6ec81fec
--- /dev/null
+++ b/apps/simple-dvt/app/src/components/shared/styles.js
@@ -0,0 +1,8 @@
+import { Box } from '@aragon/ui'
+import styled from 'styled-components'
+
+export const BoxUnpadded = styled(Box)`
+ & > div {
+ padding: 0;
+ }
+`
diff --git a/apps/simple-dvt/app/src/components/simpledvt/Primary.js b/apps/simple-dvt/app/src/components/simpledvt/Primary.js
new file mode 100644
index 000000000..acb8ea85e
--- /dev/null
+++ b/apps/simple-dvt/app/src/components/simpledvt/Primary.js
@@ -0,0 +1,42 @@
+import { useAppState } from '@aragon/api-react'
+import React from 'react'
+import { BoxUnpadded, ListItemUnformattedValue, NodeOperatorList } from '../shared'
+
+export const SimpleDVTPrimary = () => {
+ const {
+ simpleDVT,
+ } = useAppState()
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+ >
+ )
+}
diff --git a/apps/simple-dvt/app/src/components/simpledvt/Secondary.js b/apps/simple-dvt/app/src/components/simpledvt/Secondary.js
new file mode 100644
index 000000000..5aeccd53f
--- /dev/null
+++ b/apps/simple-dvt/app/src/components/simpledvt/Secondary.js
@@ -0,0 +1,26 @@
+import React from 'react'
+import { useAppState } from '@aragon/api-react'
+import { BoxUnpadded, ListItemBoolean, ListItemUnformattedValue } from '../shared'
+
+export const SimpleDVTSecondary = () => {
+ const {
+ simpleDVT,
+ } = useAppState()
+
+ return (
+ <>
+
+
+
+
+
+
+ >
+ )
+}
diff --git a/apps/simple-dvt/app/src/components/simpledvt/index.js b/apps/simple-dvt/app/src/components/simpledvt/index.js
new file mode 100644
index 000000000..a1f2ad938
--- /dev/null
+++ b/apps/simple-dvt/app/src/components/simpledvt/index.js
@@ -0,0 +1,2 @@
+export { SimpleDVTPrimary } from "./Primary"
+export { SimpleDVTSecondary } from "./Secondary"
diff --git a/apps/simple-dvt/app/src/components/styles.js b/apps/simple-dvt/app/src/components/styles.js
new file mode 100644
index 000000000..a6ec81fec
--- /dev/null
+++ b/apps/simple-dvt/app/src/components/styles.js
@@ -0,0 +1,8 @@
+import { Box } from '@aragon/ui'
+import styled from 'styled-components'
+
+export const BoxUnpadded = styled(Box)`
+ & > div {
+ padding: 0;
+ }
+`
diff --git a/apps/simple-dvt/app/src/index.js b/apps/simple-dvt/app/src/index.js
new file mode 100644
index 000000000..fc7dc90a8
--- /dev/null
+++ b/apps/simple-dvt/app/src/index.js
@@ -0,0 +1,22 @@
+import React from 'react'
+import ReactDOM from 'react-dom'
+import { AragonApi } from '@aragon/api-react'
+import App from './App'
+
+const defaultState = {
+ isSyncing: true,
+}
+
+const reducer = (state) => {
+ if (state === null) {
+ return defaultState
+ }
+ return state
+}
+
+ReactDOM.render(
+
+
+ ,
+ document.getElementById('root')
+)
diff --git a/apps/simple-dvt/app/src/script.js b/apps/simple-dvt/app/src/script.js
new file mode 100644
index 000000000..d7baeb1ce
--- /dev/null
+++ b/apps/simple-dvt/app/src/script.js
@@ -0,0 +1,154 @@
+import 'core-js/stable'
+import 'regenerator-runtime/runtime'
+import Aragon, { events } from '@aragon/api'
+
+const app = new Aragon()
+
+const createFetcher =
+ (functionName, ...args) =>
+ () =>
+ app.call(functionName, ...args).toPromise()
+
+const offset = 0
+const MAX_OPERATORS = 200
+const getNodeOperatorsIds = createFetcher('getNodeOperatorIds', offset, MAX_OPERATORS)
+
+const getNodeOperator = (nodeOperatorId) => createFetcher('getNodeOperator', nodeOperatorId, true)()
+
+const getNodeOperators = async () => {
+ const nodeOperatorsIds = await getNodeOperatorsIds()
+ const promises = nodeOperatorsIds.map((id) => getNodeOperator(id))
+ const settledPromises = await Promise.allSettled(promises)
+ const nodeOperators = settledPromises.map((settled) => settled.value)
+ return nodeOperators.map((no, i) => ({ ...no, id: nodeOperatorsIds[i] }))
+}
+
+const protocolVariables = [
+ {
+ stateKey: 'stakingModuleSummary',
+ updateEvents: [
+ 'VettedSigningKeysCountChanged',
+ 'DepositedSigningKeysCountChanged',
+ 'ExitedSigningKeysCountChanged',
+ 'TotalSigningKeysCountChanged',
+ 'StuckValidatorsCountChanged',
+ 'RefundedValidatorsCountChanged',
+ ],
+ fetch: createFetcher('getStakingModuleSummary'),
+ },
+ {
+ stateKey: 'nonce',
+ updateEvents: ['KeysOpIndexSet', 'NonceChanged'],
+ fetch: createFetcher('getNonce'),
+ },
+ {
+ stateKey: 'nodeOperatorsCount',
+ updateEvents: [],
+ fetch: createFetcher('getNodeOperatorsCount'),
+ },
+ {
+ stateKey: 'activeNodeOperatorsCount',
+ updateEvents: [],
+ fetch: createFetcher('getActiveNodeOperatorsCount'),
+ },
+ {
+ stateKey: 'stuckPenaltyDelay',
+ updateEvents: [],
+ fetch: createFetcher('getStuckPenaltyDelay'),
+ },
+ {
+ stateKey: 'nodeOperators',
+ updateEvents: [
+ 'NodeOperatorAdded',
+ 'NodeOperatorActiveSet',
+ 'NodeOperatorNameSet',
+ 'NodeOperatorRewardAddressSet',
+ 'NodeOperatorTotalKeysTrimmed',
+ '',
+ ],
+ fetch: getNodeOperators,
+ },
+ {
+ stateKey: 'stakingModuleType',
+ updateEvents: ['StakingModuleTypeSet'],
+ fetch: createFetcher('getType'),
+ },
+ {
+ stateKey: 'hasInitialized',
+ updateEvents: [],
+ fetch: createFetcher('hasInitialized'),
+ },
+ {
+ stateKey: 'initializationBlock',
+ updateEvents: [],
+ fetch: createFetcher('getInitializationBlock'),
+ },
+ {
+ stateKey: 'contractVersion',
+ updateEvents: ['ContractVersionSet'],
+ fetch: createFetcher('getContractVersion'),
+ },
+ {
+ stateKey: 'locator',
+ updateEvents: [],
+ fetch: createFetcher('getLocator'),
+ },
+]
+
+app.store(
+ async (state, { event }) => {
+ const nextState = {
+ ...state,
+ }
+
+ try {
+ if (event === events.SYNC_STATUS_SYNCING) {
+ return { ...nextState, isSyncing: true }
+ }
+
+ if (event === events.SYNC_STATUS_SYNCED) {
+ return { ...nextState, isSyncing: false }
+ }
+
+ const variable = protocolVariables.find(({ updateEvents }) => updateEvents.includes(event))
+
+ if (variable) {
+ return {
+ ...nextState,
+ [variable.stateKey]: await variable.fetch(),
+ }
+ }
+
+ return nextState
+ } catch (err) {
+ console.log(err)
+ }
+ },
+ {
+ init: initializeState(),
+ }
+)
+
+/***********************
+ * *
+ * Event Handlers *
+ * *
+ ***********************/
+
+function initializeState() {
+ return async (cachedState) => {
+ const promises = protocolVariables.map((v) => v.fetch())
+
+ const settledPromises = await Promise.allSettled(promises)
+
+ const updatedState = settledPromises.reduce((stateObject, cur, index) => {
+ stateObject[protocolVariables[index].stateKey] = cur.value
+ return stateObject
+ }, {})
+
+ return {
+ ...cachedState,
+ simpleDVT: updatedState,
+ }
+ }
+}
diff --git a/apps/simple-dvt/app/src/utils/helpers.js b/apps/simple-dvt/app/src/utils/helpers.js
new file mode 100644
index 000000000..bcf8ea9e4
--- /dev/null
+++ b/apps/simple-dvt/app/src/utils/helpers.js
@@ -0,0 +1,132 @@
+export function getEndingBasedOnNumber(
+ number,
+ wordInSingular,
+ wordInSpecialPlural
+) {
+ const numStr = number.toString()
+ const lastIndex = numStr.length - 1
+ const lastDigit = numStr[lastIndex]
+
+ switch (lastDigit) {
+ case '1':
+ return wordInSingular
+ default:
+ return wordInSpecialPlural || wordInSingular + 's'
+ }
+}
+
+export function formatKeys(keys) {
+ return '0x' + keys.join('')
+}
+
+export function formatJsonData(jsonString) {
+ const data = JSON.parse(jsonString)
+
+ const quantity = data.length
+
+ const pubkeysArray = data.map(({ pubkey }) => pubkey)
+ const pubkeys = formatKeys(pubkeysArray)
+
+ const signaturesArray = data.map(({ signature }) => signature)
+ const signatures = formatKeys(signaturesArray)
+
+ return { quantity, pubkeys, signatures }
+}
+
+export function isHexadecimal(hexString, length) {
+ if (!length) return false
+
+ const type = typeof hexString
+ if (type !== 'string') return false
+
+ const regex = new RegExp(`^[a-fA-F0-9]{${length}}$`)
+ return regex.test(hexString)
+}
+
+export function hasDuplicatePubkeys(signingKeys) {
+ const length = signingKeys.length
+ const pubkeys = signingKeys.map((key) => key.pubkey)
+
+ const pubkeySet = new Set(pubkeys)
+ if (length !== pubkeySet.size) return true
+
+ return false
+}
+
+export function hasDuplicateSigs(signingKeys) {
+ const length = signingKeys.length
+
+ const sigs = signingKeys.map((key) => key.signature)
+ const sigSet = new Set(sigs)
+ if (length !== sigSet.size) return true
+
+ return false
+}
+
+export async function myFetch(url, method = 'GET', body) {
+ const response = await fetch(url, {
+ method,
+ body: JSON.stringify(body),
+ headers: {
+ 'Content-type': 'application/json',
+ },
+ })
+ return response.json()
+}
+
+export const SIGNATURE_VERIFY_ENDPOINT = process.env.SIGNATURE_VERIFY_ENDPOINT
+
+function withoutPrefix(hexString) {
+ if (hexString.slice(0, 2) === '0x') {
+ return hexString.slice(2)
+ }
+ return hexString
+}
+
+function shortenHex(hexString) {
+ const hexNoPrefix = withoutPrefix(hexString)
+ const len = hexNoPrefix.length
+ const upTo = 4
+ return `${hexNoPrefix.slice(0, upTo)}...${hexNoPrefix.slice(len - upTo)}`
+}
+
+export async function verifySignaturesAsync(signingKeys) {
+ const body = signingKeys.map(({ pubkey, signature }) => ({
+ pubkey,
+ signature,
+ }))
+
+ const invalidSigs = await myFetch(SIGNATURE_VERIFY_ENDPOINT, 'POST', body)
+ return invalidSigs.map(shortenHex)
+}
+
+export const SUBGRAPH_ENDPOINT = process.env.SUBGRAPH_ENDPOINT
+
+function prefixEach(arrayOfkeys) {
+ return arrayOfkeys.map(({ pubkey }) => '0x' + pubkey)
+}
+
+export async function checkForDuplicatesAsync(signingKeys) {
+ const pubkeys = JSON.stringify(prefixEach(signingKeys))
+ const response = await fetch(SUBGRAPH_ENDPOINT, {
+ method: 'POST',
+ body: JSON.stringify({
+ query: `
+ query {
+ nodeOperatorSigningKeys(
+ where: {
+ pubkey_in: ${pubkeys}
+ }
+ ) {
+ pubkey
+ }
+ }
+ `,
+ }),
+ headers: {
+ 'Content-type': 'application/json',
+ },
+ })
+ const { data } = await response.json()
+ return data.nodeOperatorSigningKeys.map(({ pubkey }) => shortenHex(pubkey))
+}
diff --git a/apps/simple-dvt/arapp.json b/apps/simple-dvt/arapp.json
new file mode 100644
index 000000000..a2f706dbe
--- /dev/null
+++ b/apps/simple-dvt/arapp.json
@@ -0,0 +1,64 @@
+{
+ "roles": [
+ {
+ "name": "Manage signing keys",
+ "id": "MANAGE_SIGNING_KEYS",
+ "params": ["Node Operator id"]
+ },
+ {
+ "name": "Add node operators",
+ "id": "ADD_NODE_OPERATOR_ROLE",
+ "params": []
+ },
+ {
+ "name": "Activate/deactivate node operators",
+ "id": "SET_NODE_OPERATOR_ACTIVE_ROLE",
+ "params": ["Node Operator id", "New state"]
+ },
+ {
+ "name": "Set name of a node operator",
+ "id": "SET_NODE_OPERATOR_NAME_ROLE",
+ "params": ["Node Operator id"]
+ },
+ {
+ "name": "Set rewards address of a node operator",
+ "id": "SET_NODE_OPERATOR_ADDRESS_ROLE",
+ "params": ["Node Operator id", "New rewards address"]
+ },
+ {
+ "name": "Set staking limit of a node operator",
+ "id": "SET_NODE_OPERATOR_LIMIT_ROLE",
+ "params": ["Node Operator id", "New validators limit value"]
+ },
+ {
+ "name": "Report increments of stopped validators for a node operator",
+ "id": "REPORT_STOPPED_VALIDATORS_ROLE",
+ "params": ["Node Operator id", "Stopped validators increment"]
+ }
+ ],
+ "environments": {
+ "default": {
+ "appName": "simple-dvt.lidopm.eth",
+ "network": "development"
+ },
+ "rinkeby": {
+ "appName": "simple-dvt.lidopm.eth",
+ "network": "rinkeby"
+ },
+ "mainnet-test": {
+ "appName": "simple-dvt.lidopm-pre.eth",
+ "network": "mainnet"
+ },
+ "mainnet": {
+ "appName": "simple-dvt.lidopm.eth",
+ "network": "mainnet"
+ },
+ "localhost": {
+ "appName": "simple-dvt.lidopm.eth",
+ "network": "development",
+ "registry": "0xa16E02E87b7454126E5E10d957A927A7F5B5d2be"
+ }
+ },
+ "appName": "simple-dvt.lidopm.eth",
+ "path": "../../contracts/0.4.24/nos/NodeOperatorsRegistry.sol"
+}
diff --git a/apps/simple-dvt/hardhat.config.js b/apps/simple-dvt/hardhat.config.js
new file mode 100644
index 000000000..f8a808c9e
--- /dev/null
+++ b/apps/simple-dvt/hardhat.config.js
@@ -0,0 +1,23 @@
+require('@aragon/buidler-aragon')
+
+const baseConfig = require('../../hardhat.config.js')
+const hooks = require('./scripts/buidler-hooks')
+
+module.exports = {
+ ...baseConfig,
+ paths: {
+ ...baseConfig.paths,
+ root: '../..',
+ },
+ defaultNetwork: process.env.NETWORK_NAME || 'localhost',
+ // Aragon plugin configuration
+ aragon: {
+ ...baseConfig.aragon,
+ appServePort: 3013,
+ clientServePort: 3000,
+ appSrcPath: 'apps/simple-dvt/app/',
+ appBuildOutputPath: 'apps/simple-dvt/dist/',
+ appName: 'simple-dvt',
+ hooks, // Path to script hooks
+ },
+}
diff --git a/apps/simple-dvt/manifest.json b/apps/simple-dvt/manifest.json
new file mode 100644
index 000000000..0b93ea04d
--- /dev/null
+++ b/apps/simple-dvt/manifest.json
@@ -0,0 +1,20 @@
+{
+ "name": "SimpleDVT",
+ "author": "Lido",
+ "description": "An Aragon application for Lido Simple DVT module",
+ "details_url": "/meta/details.md",
+ "source_url": "https://github.com/lidofinance/lido-dao",
+ "icons": [
+ {
+ "src": "/meta/icon.svg",
+ "sizes": "56x56"
+ }
+ ],
+ "screenshots": [
+ {
+ "src": "/meta/screenshot-1.png"
+ }
+ ],
+ "start_url": "/index.html",
+ "script": "/script.js"
+}
diff --git a/apps/simple-dvt/scripts/buidler-hooks.js b/apps/simple-dvt/scripts/buidler-hooks.js
new file mode 100644
index 000000000..2f3f5caf2
--- /dev/null
+++ b/apps/simple-dvt/scripts/buidler-hooks.js
@@ -0,0 +1,35 @@
+/*
+ * These hooks are called by the Aragon Buidler plugin during the start task's lifecycle. Use them to perform custom tasks at certain entry points of the development build process, like deploying a token before a proxy is initialized, etc.
+ *
+ * Link them to the main buidler config file (buidler.config.js) in the `aragon.hooks` property.
+ *
+ * All hooks receive two parameters:
+ * 1) A params object that may contain other objects that pertain to the particular hook.
+ * 2) A "bre" or BuidlerRuntimeEnvironment object that contains enviroment objects like web3, Truffle artifacts, etc.
+ *
+ * Please see AragonConfigHooks, in the plugin's types for further details on these interfaces.
+ * https://github.com/aragon/buidler-aragon/blob/develop/src/types.ts#L31
+ */
+
+module.exports = {
+ // Called before a dao is deployed.
+ preDao: async ({ log }, { web3, artifacts }) => {},
+
+ // Called after a dao is deployed.
+ postDao: async ({ dao, _experimentalAppInstaller, log }, { web3, artifacts }) => {},
+
+ // Called after the app's proxy is created, but before it's initialized.
+ preInit: async ({ proxy, _experimentalAppInstaller, log }, { web3, artifacts }) => {},
+
+ // Called after the app's proxy is initialized.
+ postInit: async ({ proxy, _experimentalAppInstaller, log }, { web3, artifacts }) => {},
+
+ // Called when the start task needs to know the app proxy's init parameters.
+ // Must return an array with the proxy's init parameters.
+ getInitParams: async ({ log }, { web3, artifacts }) => {
+ return []
+ },
+
+ // Called after the app's proxy is updated with a new implementation.
+ postUpdate: async ({ proxy, log }, { web3, artifacts }) => {},
+}
diff --git a/assets/img/lido.png b/assets/img/lido.png
new file mode 100644
index 000000000..5b9d7433b
Binary files /dev/null and b/assets/img/lido.png differ
diff --git a/contracts/0.8.9/SepoliaDepositAdapter.sol b/contracts/0.8.9/SepoliaDepositAdapter.sol
new file mode 100644
index 000000000..48e5fa863
--- /dev/null
+++ b/contracts/0.8.9/SepoliaDepositAdapter.sol
@@ -0,0 +1,104 @@
+// SPDX-FileCopyrightText: 2024 Lido
+// SPDX-License-Identifier: GPL-3.0
+
+/* See contracts/COMPILERS.md */
+pragma solidity 0.8.9;
+
+import "@openzeppelin/contracts-v4.4/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts-v4.4/access/Ownable.sol";
+
+interface IDepositContract {
+ event DepositEvent(
+ bytes pubkey,
+ bytes withdrawal_credentials,
+ bytes amount,
+ bytes signature,
+ bytes index
+ );
+
+ function deposit(
+ bytes calldata pubkey,
+ bytes calldata withdrawal_credentials,
+ bytes calldata signature,
+ bytes32 deposit_data_root
+ ) external payable;
+
+ function get_deposit_root() external view returns (bytes32);
+
+ function get_deposit_count() external view returns (bytes memory);
+}
+
+// Sepolia deposit contract variant of the source code https://github.com/protolambda/testnet-dep-contract/blob/master/deposit_contract.sol
+interface ISepoliaDepositContract is IDepositContract, IERC20 { }
+
+// Sepolia testnet deposit contract have a bit different logic than the mainnet deposit contract.
+// The differences are:
+// 1. Sepolia contract require specific Bepolia token to be used for depositing. It burns this token after depositing.
+// 2. It returns the ETH to the sender after depositing.
+// This adapter is used to make the mainnet deposit contract compatible with the testnet deposit contract.
+// For further information see Sepolia deposit contract variant source code link above.
+contract SepoliaDepositAdapter is IDepositContract, Ownable {
+
+ event EthReceived(address sender, uint256 amount);
+
+ event EthRecovered(uint256 amount);
+
+ event BepoliaRecovered(uint256 amount);
+
+ error EthRecoverFailed();
+
+ error BepoliaRecoverFailed();
+
+ error DepositFailed();
+
+ ISepoliaDepositContract public immutable originalContract;
+
+ constructor(address _deposit_contract) {
+ originalContract = ISepoliaDepositContract(_deposit_contract);
+ }
+
+ function get_deposit_root() override external view returns (bytes32) {
+ return originalContract.get_deposit_root();
+ }
+
+ function get_deposit_count() override external view returns (bytes memory) {
+ return originalContract.get_deposit_count();
+ }
+
+ receive() external payable {
+ emit EthReceived(msg.sender, msg.value);
+ }
+
+ function recoverEth() external onlyOwner {
+ uint256 balance = address(this).balance;
+ // solhint-disable-next-line avoid-low-level-calls
+ (bool success,) = owner().call{value: balance}("");
+ if (!success) {
+ revert EthRecoverFailed();
+ }
+ emit EthRecovered(balance);
+ }
+
+ function recoverBepolia() external onlyOwner {
+ uint256 bepoliaOwnTokens = originalContract.balanceOf(address(this));
+ bool success = originalContract.transfer(owner(), bepoliaOwnTokens);
+ if (!success) {
+ revert BepoliaRecoverFailed();
+ }
+ emit BepoliaRecovered(bepoliaOwnTokens);
+ }
+
+ function deposit(
+ bytes calldata pubkey,
+ bytes calldata withdrawal_credentials,
+ bytes calldata signature,
+ bytes32 deposit_data_root
+ ) override external payable {
+ originalContract.deposit{value: msg.value}(pubkey, withdrawal_credentials, signature, deposit_data_root);
+ // solhint-disable-next-line avoid-low-level-calls
+ (bool success,) = owner().call{value: msg.value}("");
+ if (!success) {
+ revert DepositFailed();
+ }
+ }
+}
diff --git a/contracts/COMPILERS.md b/contracts/COMPILERS.md
index 6d1825937..bd07c67ee 100644
--- a/contracts/COMPILERS.md
+++ b/contracts/COMPILERS.md
@@ -2,17 +2,10 @@
For Lido project coordination, governance and funds management we use [Aragon](https://aragon.org/dao), a well-developed and proven DAO Framework. The current stable release of its Kernel, [4.4.0](https://github.com/aragon/aragonOS/tree/v4.4.0) is fixed on the specific compiler version - [solc 0.4.24](https://solidity.readthedocs.io/en/v0.4.24/), that is currently outdated. Keeping security and consistency in mind, we decided to stay on an older yet proven combination - for all the contracts under Aragon management (`Lido`, `stETH`, `LegacyOracle`) we use solc 0.4.24 release.
-cstETH token, that acts as autonomous wrapper and not governed by Aragon, was inherited from OpenZeppelin's library, using one of its stable releases [3.1.0](https://github.com/OpenZeppelin/openzeppelin-contracts/releases/tag/v3.1.0).
+For the other contracts the newer compiler versions are used.
# How to compile
-Separately:
-
-```bash
-yarn compile:4
-yarn compile:6
-```
-
All at once:
```bash
diff --git a/contracts/common/interfaces/IGateSealFactory.sol b/contracts/common/interfaces/IGateSealFactory.sol
new file mode 100644
index 000000000..b2ffaaec5
--- /dev/null
+++ b/contracts/common/interfaces/IGateSealFactory.sol
@@ -0,0 +1,20 @@
+// SPDX-FileCopyrightText: 2023 Lido
+// SPDX-License-Identifier: GPL-3.0
+
+// See contracts/COMPILERS.md
+// solhint-disable-next-line
+pragma solidity >=0.4.24 <0.9.0;
+
+// https://github.com/lidofinance/gate-seals/blob/main/contracts/GateSealFactory.vy
+interface IGateSealFactory {
+
+ event GateSealCreated(address gate_seal);
+
+ function create_gate_seal(
+ address _sealing_committee,
+ uint256 _seal_duration_seconds,
+ address[] memory _sealables,
+ uint256 _expiry_timestamp
+ ) external;
+
+}
diff --git a/deployed-holesky.json b/deployed-holesky.json
new file mode 100644
index 000000000..fbac35279
--- /dev/null
+++ b/deployed-holesky.json
@@ -0,0 +1,732 @@
+{
+ "accountingOracle": {
+ "deployParameters": {
+ "consensusVersion": 1
+ },
+ "proxy": {
+ "contract": "contracts/0.8.9/proxy/OssifiableProxy.sol",
+ "address": "0x4E97A3972ce8511D87F334dA17a2C332542a5246",
+ "constructorArgs": [
+ "0x6AcA050709469F1f98d8f40f68b1C83B533cd2b2",
+ "0x22896Bfc68814BFD855b1a167255eE497006e730",
+ "0x"
+ ]
+ },
+ "implementation": {
+ "contract": "contracts/0.8.9/oracle/AccountingOracle.sol",
+ "address": "0x6AcA050709469F1f98d8f40f68b1C83B533cd2b2",
+ "constructorArgs": [
+ "0x28FAB2059C713A7F9D8c86Db49f9bb0e96Af1ef8",
+ "0x3F1c547b21f65e10480dE3ad8E19fAAC46C95034",
+ "0x072f72BE3AcFE2c52715829F2CD9061A6C8fF019",
+ 12,
+ 1695902400
+ ]
+ }
+ },
+ "apmRepoBaseAddress": "0x8959360c48D601a6817BAf2449E5D00cC543FA3A",
+ "app:aragon-agent": {
+ "implementation": {
+ "contract": "@aragon/apps-agent/contracts/Agent.sol",
+ "address": "0xF4aDA7Ff34c508B9Af2dE4160B6078D2b58FD46B",
+ "constructorArgs": []
+ },
+ "aragonApp": {
+ "name": "aragon-agent",
+ "fullName": "aragon-agent.lidopm.eth",
+ "id": "0x701a4fd1f5174d12a0f1d9ad2c88d0ad11ab6aad0ac72b7d9ce621815f8016a9"
+ },
+ "proxy": {
+ "address": "0xE92329EC7ddB11D25e25b3c21eeBf11f15eB325d",
+ "contract": "@aragon/os/contracts/apps/AppProxyUpgradeable.sol",
+ "constructorArgs": [
+ "0x3b03f75Ec541Ca11a223bB58621A3146246E1644",
+ "0x701a4fd1f5174d12a0f1d9ad2c88d0ad11ab6aad0ac72b7d9ce621815f8016a9",
+ "0x8129fc1c"
+ ]
+ }
+ },
+ "app:aragon-finance": {
+ "implementation": {
+ "contract": "@aragon/apps-finance/contracts/Finance.sol",
+ "address": "0x1a76ED38B14C768e02b96A879d89Db18AC83EC53",
+ "constructorArgs": []
+ },
+ "aragonApp": {
+ "name": "aragon-finance",
+ "fullName": "aragon-finance.lidopm.eth",
+ "id": "0x5c9918c99c4081ca9459c178381be71d9da40e49e151687da55099c49a4237f1"
+ },
+ "proxy": {
+ "address": "0xf0F281E5d7FBc54EAFcE0dA225CDbde04173AB16",
+ "contract": "@aragon/os/contracts/apps/AppProxyUpgradeable.sol",
+ "constructorArgs": [
+ "0x3b03f75Ec541Ca11a223bB58621A3146246E1644",
+ "0x5c9918c99c4081ca9459c178381be71d9da40e49e151687da55099c49a4237f1",
+ "0x1798de81000000000000000000000000e92329ec7ddb11d25e25b3c21eebf11f15eb325d0000000000000000000000000000000000000000000000000000000000278d00"
+ ]
+ }
+ },
+ "app:aragon-token-manager": {
+ "implementation": {
+ "contract": "@aragon/apps-lido/apps/token-manager/contracts/TokenManager.sol",
+ "address": "0x6f0b994E6827faC1fDb58AF66f365676247bAD71",
+ "constructorArgs": []
+ },
+ "aragonApp": {
+ "name": "aragon-token-manager",
+ "fullName": "aragon-token-manager.lidopm.eth",
+ "id": "0xcd567bdf93dd0f6acc3bc7f2155f83244d56a65abbfbefb763e015420102c67b"
+ },
+ "proxy": {
+ "address": "0xFaa1692c6eea8eeF534e7819749aD93a1420379A",
+ "contract": "@aragon/os/contracts/apps/AppProxyUpgradeable.sol",
+ "constructorArgs": [
+ "0x3b03f75Ec541Ca11a223bB58621A3146246E1644",
+ "0xcd567bdf93dd0f6acc3bc7f2155f83244d56a65abbfbefb763e015420102c67b",
+ "0x"
+ ]
+ }
+ },
+ "app:aragon-voting": {
+ "implementation": {
+ "contract": "@aragon/apps-lido/apps/voting/contracts/Voting.sol",
+ "address": "0x994c92228803e8b2D0fb8a610AbCB47412EeF8eF",
+ "constructorArgs": []
+ },
+ "aragonApp": {
+ "name": "aragon-voting",
+ "fullName": "aragon-voting.lidopm.eth",
+ "id": "0x0abcd104777321a82b010357f20887d61247493d89d2e987ff57bcecbde00e1e"
+ },
+ "proxy": {
+ "address": "0xdA7d2573Df555002503F29aA4003e398d28cc00f",
+ "contract": "@aragon/os/contracts/apps/AppProxyUpgradeable.sol",
+ "constructorArgs": [
+ "0x3b03f75Ec541Ca11a223bB58621A3146246E1644",
+ "0x0abcd104777321a82b010357f20887d61247493d89d2e987ff57bcecbde00e1e",
+ "0x13e0945300000000000000000000000014ae7daeecdf57034f3e9db8564e46dba8d9734400000000000000000000000000000000000000000000000006f05b59d3b2000000000000000000000000000000000000000000000000000000b1a2bc2ec500000000000000000000000000000000000000000000000000000000000000000384000000000000000000000000000000000000000000000000000000000000012c"
+ ]
+ }
+ },
+ "app:lido": {
+ "implementation": {
+ "contract": "contracts/0.4.24/Lido.sol",
+ "address": "0x59034815464d18134A55EED3702b535D8A32c52b",
+ "constructorArgs": []
+ },
+ "aragonApp": {
+ "name": "lido",
+ "fullName": "lido.lidopm.eth",
+ "id": "0x3ca7c3e38968823ccb4c78ea688df41356f182ae1d159e4ee608d30d68cef320"
+ },
+ "proxy": {
+ "address": "0x3F1c547b21f65e10480dE3ad8E19fAAC46C95034",
+ "contract": "@aragon/os/contracts/apps/AppProxyUpgradeable.sol",
+ "constructorArgs": [
+ "0x3b03f75Ec541Ca11a223bB58621A3146246E1644",
+ "0x3ca7c3e38968823ccb4c78ea688df41356f182ae1d159e4ee608d30d68cef320",
+ "0x"
+ ]
+ }
+ },
+ "app:node-operators-registry": {
+ "implementation": {
+ "contract": "contracts/0.4.24/nos/NodeOperatorsRegistry.sol",
+ "address": "0xE0270CF2564d81E02284e16539F59C1B5a4718fE",
+ "constructorArgs": []
+ },
+ "aragonApp": {
+ "name": "node-operators-registry",
+ "fullName": "node-operators-registry.lidopm.eth",
+ "id": "0x7071f283424072341f856ac9e947e7ec0eb68719f757a7e785979b6b8717579d"
+ },
+ "proxy": {
+ "address": "0x595F64Ddc3856a3b5Ff4f4CC1d1fb4B46cFd2bAC",
+ "contract": "@aragon/os/contracts/apps/AppProxyUpgradeable.sol",
+ "constructorArgs": [
+ "0x3b03f75Ec541Ca11a223bB58621A3146246E1644",
+ "0x7071f283424072341f856ac9e947e7ec0eb68719f757a7e785979b6b8717579d",
+ "0x"
+ ]
+ }
+ },
+ "app:oracle": {
+ "implementation": {
+ "contract": "contracts/0.4.24/oracle/LegacyOracle.sol",
+ "address": "0xcE4B3D5bd6259F5dD73253c51b17e5a87bb9Ee64",
+ "constructorArgs": []
+ },
+ "aragonApp": {
+ "name": "oracle",
+ "fullName": "oracle.lidopm.eth",
+ "id": "0x8b47ba2a8454ec799cd91646e7ec47168e91fd139b23f017455f3e5898aaba93"
+ },
+ "proxy": {
+ "address": "0x072f72BE3AcFE2c52715829F2CD9061A6C8fF019",
+ "contract": "@aragon/os/contracts/apps/AppProxyUpgradeable.sol",
+ "constructorArgs": [
+ "0x3b03f75Ec541Ca11a223bB58621A3146246E1644",
+ "0x8b47ba2a8454ec799cd91646e7ec47168e91fd139b23f017455f3e5898aaba93",
+ "0x"
+ ]
+ }
+ },
+ "app:simple-dvt": {
+ "stakingRouterModuleParams": {
+ "moduleName": "SimpleDVT",
+ "moduleType": "curated-onchain-v1",
+ "targetShare": 50,
+ "moduleFee": 800,
+ "treasuryFee": 200,
+ "penaltyDelay": 86400,
+ "easyTrackTrustedCaller": "0xD76001b33b23452243E2FDa833B6e7B8E3D43198",
+ "easyTrackAddress": "0x1763b9ED3586B08AE796c7787811a2E1bc16163a",
+ "easyTrackFactories": {
+ "AddNodeOperators": "0xeF5233A5bbF243149E35B353A73FFa8931FDA02b",
+ "ActivateNodeOperators": "0x5b4A9048176D5bA182ceec8e673D8aA6927A40D6",
+ "DeactivateNodeOperators": "0x88d247cdf4ff4A4AAA8B3DD9dd22D1b89219FB3B",
+ "SetVettedValidatorsLimits": "0x30Cb36DBb0596aD9Cf5159BD2c4B1456c18e47E8",
+ "SetNodeOperatorNames": "0x4792BaC0a262200fA7d3b68e7622bFc1c2c3a72d",
+ "SetNodeOperatorRewardAddresses": "0x6Bfc576018C7f3D2a9180974E5c8e6CFa021f617",
+ "UpdateTargetValidatorLimits": "0xC91a676A69Eb49be9ECa1954fE6fc861AE07A9A2",
+ "ChangeNodeOperatorManagers": "0xb8C4728bc0826bA5864D02FA53148de7A44C2f7E"
+ }
+ },
+ "aragonApp": {
+ "name": "simple-dvt",
+ "fullName": "simple-dvt.lidopm.eth",
+ "id": "0xe1635b63b5f7b5e545f2a637558a4029dea7905361a2f0fc28c66e9136cf86a4"
+ },
+ "proxy": {
+ "address": "0x11a93807078f8BB880c1BD0ee4C387537de4b4b6",
+ "contract": "@aragon/os/contracts/apps/AppProxyUpgradeable.sol",
+ "constructorArgs": [
+ "0x3b03f75Ec541Ca11a223bB58621A3146246E1644",
+ "0xe1635b63b5f7b5e545f2a637558a4029dea7905361a2f0fc28c66e9136cf86a4",
+ "0x"
+ ]
+ },
+ "fullName": "simple-dvt.lidopm.eth",
+ "name": "simple-dvt",
+ "id": "0xe1635b63b5f7b5e545f2a637558a4029dea7905361a2f0fc28c66e9136cf86a4",
+ "ipfsCid": "QmaSSujHCGcnFuetAPGwVW5BegaMBvn5SCsgi3LSfvraSo",
+ "contentURI": "0x697066733a516d615353756a484347636e4675657441504777565735426567614d42766e355343736769334c5366767261536f",
+ "implementation": "0xE0270CF2564d81E02284e16539F59C1B5a4718fE",
+ "contract": "NodeOperatorsRegistry"
+ },
+ "aragon-acl": {
+ "implementation": {
+ "contract": "@aragon/os/contracts/acl/ACL.sol",
+ "address": "0xF1A087E055EA1C11ec3B540795Bd1A544e6dcbe9",
+ "constructorArgs": []
+ },
+ "proxy": {
+ "address": "0xfd1E42595CeC3E83239bf8dFc535250e7F48E0bC",
+ "constructorArgs": [
+ "0x3b03f75Ec541Ca11a223bB58621A3146246E1644",
+ "0xe3262375f45a6e2026b7e7b18c2b807434f2508fe1a2a3dfb493c7df8f4aad6a",
+ "0x00"
+ ],
+ "contract": "@aragon/os/contracts/apps/AppProxyUpgradeable.sol"
+ },
+ "aragonApp": {
+ "name": "aragon-acl",
+ "id": "0xe3262375f45a6e2026b7e7b18c2b807434f2508fe1a2a3dfb493c7df8f4aad6a"
+ }
+ },
+ "aragon-apm-registry": {
+ "implementation": {
+ "contract": "@aragon/os/contracts/apm/APMRegistry.sol",
+ "address": "0x3EcF7190312F50043DB0494bA0389135Fc3833F3",
+ "constructorArgs": [
+ "0x3b03f75Ec541Ca11a223bB58621A3146246E1644",
+ "0x9089af016eb74d66811e1c39c1eef86fdcdb84b5665a4884ebf62339c2613991",
+ "0x00"
+ ]
+ },
+ "proxy": {
+ "address": "0xB576A85c310CC7Af5C106ab26d2942fA3a5ea94A",
+ "contract": "@aragon/os/contracts/apps/AppProxyUpgradeable.sol"
+ },
+ "factory": {
+ "address": "0x54eF0022cc769344D0cBCeF12e051281cCBb9fad",
+ "contract": "@aragon/os/contracts/factory/APMRegistryFactory.sol",
+ "constructorArgs": [
+ "0xB33f9AE6C34D8cC59A48fd9973C64488f00fa64F",
+ "0x3EcF7190312F50043DB0494bA0389135Fc3833F3",
+ "0x8959360c48D601a6817BAf2449E5D00cC543FA3A",
+ "0x7B133ACab5Cec7B90FB13CCf68d6568f8A051EcE",
+ "0x4327d1Fc6E5fa0326CCAE737F67C066c50BcC258",
+ "0x0000000000000000000000000000000000000000"
+ ]
+ }
+ },
+ "aragon-app-repo-agent": {
+ "implementation": {
+ "contract": "@aragon/os/contracts/apm/Repo.sol",
+ "address": "0x8959360c48D601a6817BAf2449E5D00cC543FA3A",
+ "constructorArgs": []
+ },
+ "proxy": {
+ "address": "0xe7b4567913AaF2bD54A26E742cec22727D8109eA",
+ "contract": "@aragon/os/contracts/apps/AppProxyUpgradeable.sol",
+ "constructorArgs": []
+ }
+ },
+ "aragon-app-repo-finance": {
+ "implementation": {
+ "contract": "@aragon/os/contracts/apm/Repo.sol",
+ "address": "0x8959360c48D601a6817BAf2449E5D00cC543FA3A",
+ "constructorArgs": []
+ },
+ "proxy": {
+ "address": "0x0df65b7c78Dc42a872010d031D3601C284D8fE71",
+ "contract": "@aragon/os/contracts/apps/AppProxyUpgradeable.sol",
+ "constructorArgs": []
+ }
+ },
+ "aragon-app-repo-lido": {
+ "implementation": {
+ "contract": "@aragon/os/contracts/apm/Repo.sol",
+ "address": "0x8959360c48D601a6817BAf2449E5D00cC543FA3A",
+ "constructorArgs": []
+ },
+ "proxy": {
+ "address": "0xA37fb4C41e7D30af5172618a863BBB0f9042c604",
+ "contract": "@aragon/os/contracts/apps/AppProxyUpgradeable.sol",
+ "constructorArgs": []
+ }
+ },
+ "aragon-app-repo-node-operators-registry": {
+ "implementation": {
+ "contract": "@aragon/os/contracts/apm/Repo.sol",
+ "address": "0x8959360c48D601a6817BAf2449E5D00cC543FA3A",
+ "constructorArgs": []
+ },
+ "proxy": {
+ "address": "0x4E8970d148CB38460bE9b6ddaab20aE2A74879AF",
+ "contract": "@aragon/os/contracts/apps/AppProxyUpgradeable.sol",
+ "constructorArgs": []
+ }
+ },
+ "aragon-app-repo-oracle": {
+ "implementation": {
+ "contract": "@aragon/os/contracts/apm/Repo.sol",
+ "address": "0x8959360c48D601a6817BAf2449E5D00cC543FA3A",
+ "constructorArgs": []
+ },
+ "proxy": {
+ "address": "0xB3d74c319C0C792522705fFD3097f873eEc71764",
+ "contract": "@aragon/os/contracts/apps/AppProxyUpgradeable.sol",
+ "constructorArgs": []
+ }
+ },
+ "aragon-app-repo-token-manager": {
+ "implementation": {
+ "contract": "@aragon/os/contracts/apm/Repo.sol",
+ "address": "0x8959360c48D601a6817BAf2449E5D00cC543FA3A",
+ "constructorArgs": []
+ },
+ "proxy": {
+ "address": "0xD327b4Fb87fa01599DaD491Aa63B333c44C74472",
+ "contract": "@aragon/os/contracts/apps/AppProxyUpgradeable.sol",
+ "constructorArgs": []
+ }
+ },
+ "aragon-app-repo-voting": {
+ "implementation": {
+ "contract": "@aragon/os/contracts/apm/Repo.sol",
+ "address": "0x8959360c48D601a6817BAf2449E5D00cC543FA3A",
+ "constructorArgs": []
+ },
+ "proxy": {
+ "address": "0x2997EA0D07D79038D83Cb04b3BB9A2Bc512E3fDA",
+ "contract": "@aragon/os/contracts/apps/AppProxyUpgradeable.sol",
+ "constructorArgs": []
+ }
+ },
+ "aragon-evm-script-registry": {
+ "proxy": {
+ "address": "0xE1200ae048163B67D69Bc0492bF5FddC3a2899C0",
+ "constructorArgs": [
+ "0x3b03f75Ec541Ca11a223bB58621A3146246E1644",
+ "0xddbcfd564f642ab5627cf68b9b7d374fb4f8a36e941a75d89c87998cef03bd61",
+ "0x8129fc1c"
+ ],
+ "contract": "@aragon/os/contracts/apps/AppProxyPinned.sol"
+ },
+ "aragonApp": {
+ "name": "aragon-evm-script-registry",
+ "id": "0xddbcfd564f642ab5627cf68b9b7d374fb4f8a36e941a75d89c87998cef03bd61"
+ },
+ "implementation": {
+ "address": "0x923B9Cab88E4a1d3de7EE921dEFBF9e2AC6e0791",
+ "contract": "@aragon/os/contracts/evmscript/EVMScriptRegistry.sol",
+ "constructorArgs": []
+ }
+ },
+ "aragon-kernel": {
+ "implementation": {
+ "contract": "@aragon/os/contracts/kernel/Kernel.sol",
+ "address": "0x34c0cbf9836FD945423bD3d2d72880da9d068E5F",
+ "constructorArgs": [true]
+ },
+ "proxy": {
+ "address": "0x3b03f75Ec541Ca11a223bB58621A3146246E1644",
+ "contract": "@aragon/os/contracts/kernel/KernelProxy.sol",
+ "constructorArgs": ["0x34c0cbf9836FD945423bD3d2d72880da9d068E5F"]
+ }
+ },
+ "aragonEnsLabelName": "aragonpm",
+ "aragonEnsNode": "0x9065c3e7f7b7ef1ef4e53d2d0b8e0cef02874ab020c1ece79d5f0d3d0111c0ba",
+ "aragonEnsNodeName": "aragonpm.eth",
+ "aragonIDAddress": "0xCA01225e211AB0c6EFCD3aCc64D85465e4D8ab53",
+ "aragonIDConstructorArgs": [
+ "0x4327d1Fc6E5fa0326CCAE737F67C066c50BcC258",
+ "0x2B725cBA5F75c3B61bb5E37454a7090fb11c757E",
+ "0x7e74a86b6e146964fb965db04dc2590516da77f720bb6759337bf5632415fd86"
+ ],
+ "aragonIDEnsNode": "0x7e74a86b6e146964fb965db04dc2590516da77f720bb6759337bf5632415fd86",
+ "aragonIDEnsNodeName": "aragonid.eth",
+ "burner": {
+ "deployParameters": {
+ "totalCoverSharesBurnt": "0",
+ "totalNonCoverSharesBurnt": "0"
+ },
+ "contract": "contracts/0.8.9/Burner.sol",
+ "address": "0x4E46BD7147ccf666E1d73A3A456fC7a68de82eCA",
+ "constructorArgs": [
+ "0x22896Bfc68814BFD855b1a167255eE497006e730",
+ "0xE92329EC7ddB11D25e25b3c21eeBf11f15eB325d",
+ "0x3F1c547b21f65e10480dE3ad8E19fAAC46C95034",
+ "0",
+ "0"
+ ]
+ },
+ "callsScript": {
+ "address": "0xAa8B4F258a4817bfb0058b861447878168ddf7B0",
+ "contract": "@aragon/os/contracts/evmscript/executors/CallsScript.sol",
+ "constructorArgs": []
+ },
+ "chainId": 17000,
+ "chainSpec": {
+ "slotsPerEpoch": 32,
+ "secondsPerSlot": 12,
+ "genesisTime": 1695902400,
+ "depositContract": "0x4242424242424242424242424242424242424242"
+ },
+ "createAppReposTx": "0xd8a9b10e16b5e75b984c90154a9cb51fbb06bf560a3c424e2e7ad81951008502",
+ "daoAragonId": "lido-dao",
+ "daoFactoryAddress": "0xB33f9AE6C34D8cC59A48fd9973C64488f00fa64F",
+ "daoFactoryConstructorArgs": [
+ "0x34c0cbf9836FD945423bD3d2d72880da9d068E5F",
+ "0xF1A087E055EA1C11ec3B540795Bd1A544e6dcbe9",
+ "0x11E7591F83360d0Bc238c8AB9e50B6D2B7566aDc"
+ ],
+ "daoInitialSettings": {
+ "voting": {
+ "minSupportRequired": "500000000000000000",
+ "minAcceptanceQuorum": "50000000000000000",
+ "voteDuration": 900,
+ "objectionPhaseDuration": 300
+ },
+ "fee": {
+ "totalPercent": 10,
+ "treasuryPercent": 50,
+ "nodeOperatorsPercent": 50
+ },
+ "token": {
+ "name": "TEST Lido DAO Token",
+ "symbol": "TLDO"
+ }
+ },
+ "deployCommit": "eda16728a7c80f1bb55c3b91c668aae190a1efb0",
+ "deployer": "0x22896Bfc68814BFD855b1a167255eE497006e730",
+ "depositSecurityModule": {
+ "deployParameters": {
+ "maxDepositsPerBlock": 150,
+ "minDepositBlockDistance": 5,
+ "pauseIntentValidityPeriodBlocks": 6646
+ },
+ "contract": "contracts/0.8.9/DepositSecurityModule.sol",
+ "address": "0x045dd46212A178428c088573A7d102B9d89a022A",
+ "constructorArgs": [
+ "0x3F1c547b21f65e10480dE3ad8E19fAAC46C95034",
+ "0x4242424242424242424242424242424242424242",
+ "0xd6EbF043D30A7fe46D1Db32BA90a0A51207FE229",
+ 150,
+ 5,
+ 6646
+ ]
+ },
+ "dummyEmptyContract": {
+ "contract": "contracts/0.8.9/test_helpers/DummyEmptyContract.sol",
+ "address": "0x5F4FEf09Cbd5ad743632Fb869E80294933473f0B",
+ "constructorArgs": []
+ },
+ "eip712StETH": {
+ "contract": "contracts/0.8.9/EIP712StETH.sol",
+ "address": "0xE154732c5Eab277fd88a9fF6Bdff7805eD97BCB1",
+ "constructorArgs": ["0x3F1c547b21f65e10480dE3ad8E19fAAC46C95034"]
+ },
+ "ensAddress": "0x4327d1Fc6E5fa0326CCAE737F67C066c50BcC258",
+ "ensFactoryAddress": "0xADba3e3122F2Da8F7B07723a3e1F1cEDe3fe8d7d",
+ "ensFactoryConstructorArgs": [],
+ "ensSubdomainRegistrarBaseAddress": "0x7B133ACab5Cec7B90FB13CCf68d6568f8A051EcE",
+ "evmScriptRegistryFactoryAddress": "0x11E7591F83360d0Bc238c8AB9e50B6D2B7566aDc",
+ "evmScriptRegistryFactoryConstructorArgs": [],
+ "executionLayerRewardsVault": {
+ "contract": "contracts/0.8.9/LidoExecutionLayerRewardsVault.sol",
+ "address": "0xE73a3602b99f1f913e72F8bdcBC235e206794Ac8",
+ "constructorArgs": ["0x3F1c547b21f65e10480dE3ad8E19fAAC46C95034", "0xE92329EC7ddB11D25e25b3c21eeBf11f15eB325d"]
+ },
+ "gateSeal": {
+ "factoryAddress": "0x1134F7077055b0B3559BE52AfeF9aA22A0E1eEC2",
+ "sealDuration": 518400,
+ "expiryTimestamp": 1714521600,
+ "sealingCommittee": "0xCD1f9954330AF39a74Fd6e7B25781B4c24ee373f",
+ "address": "0x7f6FA688d4C12a2d51936680b241f3B0F0F9ca60"
+ },
+ "hashConsensusForAccountingOracle": {
+ "deployParameters": {
+ "fastLaneLengthSlots": 10,
+ "epochsPerFrame": 12
+ },
+ "contract": "contracts/0.8.9/oracle/HashConsensus.sol",
+ "address": "0xa067FC95c22D51c3bC35fd4BE37414Ee8cc890d2",
+ "constructorArgs": [
+ 32,
+ 12,
+ 1695902400,
+ 12,
+ 10,
+ "0x22896Bfc68814BFD855b1a167255eE497006e730",
+ "0x4E97A3972ce8511D87F334dA17a2C332542a5246"
+ ]
+ },
+ "hashConsensusForValidatorsExitBusOracle": {
+ "deployParameters": {
+ "fastLaneLengthSlots": 10,
+ "epochsPerFrame": 4
+ },
+ "contract": "contracts/0.8.9/oracle/HashConsensus.sol",
+ "address": "0xe77Cf1A027d7C10Ee6bb7Ede5E922a181FF40E8f",
+ "constructorArgs": [
+ 32,
+ 12,
+ 1695902400,
+ 4,
+ 10,
+ "0x22896Bfc68814BFD855b1a167255eE497006e730",
+ "0xffDDF7025410412deaa05E3E1cE68FE53208afcb"
+ ]
+ },
+ "ldo": {
+ "address": "0x14ae7daeecdf57034f3E9db8564e46Dba8D97344",
+ "contract": "@aragon/minime/contracts/MiniMeToken.sol",
+ "constructorArgs": [
+ "0x15ef666c9620C0f606Ba35De2aF668fe987E26ae",
+ "0x0000000000000000000000000000000000000000",
+ 0,
+ "TEST Lido DAO Token",
+ 18,
+ "TLDO",
+ true
+ ]
+ },
+ "legacyOracle": {
+ "deployParameters": {
+ "lastCompletedEpochId": 0
+ }
+ },
+ "lidoApm": {
+ "deployArguments": [
+ "0x93cdeb708b7545dc668eb9280176169d1c33cfd8ed6f04690a0bcc88a93fc4ae",
+ "0x90a9580abeb24937fc658e497221c81ce8553b560304f9525821f32b17dbdaec"
+ ],
+ "deployTx": "0x2fac1c172a250736c34d16d3a721d2916abac0de0dea67d79955346a1f4345a2",
+ "address": "0x4605Dc9dC4BD0442F850eB8226B94Dd0e27C3Ce7"
+ },
+ "lidoApmEnsName": "lidopm.eth",
+ "lidoApmEnsRegDurationSec": 94608000,
+ "lidoLocator": {
+ "proxy": {
+ "contract": "contracts/0.8.9/proxy/OssifiableProxy.sol",
+ "address": "0x28FAB2059C713A7F9D8c86Db49f9bb0e96Af1ef8",
+ "constructorArgs": [
+ "0x5F4FEf09Cbd5ad743632Fb869E80294933473f0B",
+ "0x22896Bfc68814BFD855b1a167255eE497006e730",
+ "0x"
+ ]
+ },
+ "implementation": {
+ "contract": "contracts/0.8.9/LidoLocator.sol",
+ "address": "0xDba5Ad530425bb1b14EECD76F1b4a517780de537",
+ "constructorArgs": [
+ [
+ "0x4E97A3972ce8511D87F334dA17a2C332542a5246",
+ "0x045dd46212A178428c088573A7d102B9d89a022A",
+ "0xE73a3602b99f1f913e72F8bdcBC235e206794Ac8",
+ "0x072f72BE3AcFE2c52715829F2CD9061A6C8fF019",
+ "0x3F1c547b21f65e10480dE3ad8E19fAAC46C95034",
+ "0xF0d576c7d934bBeCc68FE15F1c5DAF98ea2B78bb",
+ "0x072f72BE3AcFE2c52715829F2CD9061A6C8fF019",
+ "0x4E46BD7147ccf666E1d73A3A456fC7a68de82eCA",
+ "0xd6EbF043D30A7fe46D1Db32BA90a0A51207FE229",
+ "0xE92329EC7ddB11D25e25b3c21eeBf11f15eB325d",
+ "0xffDDF7025410412deaa05E3E1cE68FE53208afcb",
+ "0xc7cc160b58F8Bb0baC94b80847E2CF2800565C50",
+ "0xF0179dEC45a37423EAD4FaD5fCb136197872EAd9",
+ "0xC01fC1F2787687Bc656EAc0356ba9Db6e6b7afb7"
+ ]
+ ]
+ }
+ },
+ "lidoTemplate": {
+ "contract": "contracts/0.4.24/template/LidoTemplate.sol",
+ "address": "0x0e065Dd0Bc85Ca53cfDAf8D9ed905e692260De2E",
+ "constructorArgs": [
+ "0x22896Bfc68814BFD855b1a167255eE497006e730",
+ "0xB33f9AE6C34D8cC59A48fd9973C64488f00fa64F",
+ "0x4327d1Fc6E5fa0326CCAE737F67C066c50BcC258",
+ "0x15ef666c9620C0f606Ba35De2aF668fe987E26ae",
+ "0xCA01225e211AB0c6EFCD3aCc64D85465e4D8ab53",
+ "0x54eF0022cc769344D0cBCeF12e051281cCBb9fad"
+ ],
+ "deployBlock": 30581
+ },
+ "lidoTemplateCreateStdAppReposTx": "0x3f5b8918667bd3e971606a54a907798720158587df355a54ce07c0d0f9750d3c",
+ "lidoTemplateNewDaoTx": "0x3346246f09f91ffbc260b6c300b11ababce9f5ca54d7880a277860961f343112",
+ "miniMeTokenFactoryAddress": "0x15ef666c9620C0f606Ba35De2aF668fe987E26ae",
+ "miniMeTokenFactoryConstructorArgs": [],
+ "networkId": 17000,
+ "newDaoTx": "0x3346246f09f91ffbc260b6c300b11ababce9f5ca54d7880a277860961f343112",
+ "nodeOperatorsRegistry": {
+ "deployParameters": {
+ "stakingModuleTypeId": "curated-onchain-v1",
+ "stuckPenaltyDelay": 172800
+ }
+ },
+ "oracleDaemonConfig": {
+ "contract": "contracts/0.8.9/OracleDaemonConfig.sol",
+ "address": "0xC01fC1F2787687Bc656EAc0356ba9Db6e6b7afb7",
+ "constructorArgs": ["0x22896Bfc68814BFD855b1a167255eE497006e730", []],
+ "deployParameters": {
+ "NORMALIZED_CL_REWARD_PER_EPOCH": 64,
+ "NORMALIZED_CL_REWARD_MISTAKE_RATE_BP": 1000,
+ "REBASE_CHECK_NEAREST_EPOCH_DISTANCE": 1,
+ "REBASE_CHECK_DISTANT_EPOCH_DISTANCE": 23,
+ "VALIDATOR_DELAYED_TIMEOUT_IN_SLOTS": 7200,
+ "VALIDATOR_DELINQUENT_TIMEOUT_IN_SLOTS": 28800,
+ "NODE_OPERATOR_NETWORK_PENETRATION_THRESHOLD_BP": 100,
+ "PREDICTION_DURATION_IN_SLOTS": 50400,
+ "FINALIZATION_MAX_NEGATIVE_REBASE_EPOCH_SHIFT": 1350
+ }
+ },
+ "oracleReportSanityChecker": {
+ "deployParameters": {
+ "churnValidatorsPerDayLimit": 1500,
+ "oneOffCLBalanceDecreaseBPLimit": 500,
+ "annualBalanceIncreaseBPLimit": 1000,
+ "simulatedShareRateDeviationBPLimit": 250,
+ "maxValidatorExitRequestsPerReport": 2000,
+ "maxAccountingExtraDataListItemsCount": 100,
+ "maxNodeOperatorsPerExtraDataItemCount": 100,
+ "requestTimestampMargin": 128,
+ "maxPositiveTokenRebase": 5000000
+ },
+ "contract": "contracts/0.8.9/sanity_checks/OracleReportSanityChecker.sol",
+ "address": "0xF0d576c7d934bBeCc68FE15F1c5DAF98ea2B78bb",
+ "constructorArgs": [
+ "0x28FAB2059C713A7F9D8c86Db49f9bb0e96Af1ef8",
+ "0x22896Bfc68814BFD855b1a167255eE497006e730",
+ [1500, 500, 1000, 250, 2000, 100, 100, 128, 5000000],
+ [[], [], [], [], [], [], [], [], [], []]
+ ]
+ },
+ "stakingRouter": {
+ "proxy": {
+ "contract": "contracts/0.8.9/proxy/OssifiableProxy.sol",
+ "address": "0xd6EbF043D30A7fe46D1Db32BA90a0A51207FE229",
+ "constructorArgs": [
+ "0x32f236423928c2c138F46351D9E5FD26331B1aa4",
+ "0x22896Bfc68814BFD855b1a167255eE497006e730",
+ "0x"
+ ]
+ },
+ "implementation": {
+ "contract": "contracts/0.8.9/StakingRouter.sol",
+ "address": "0x32f236423928c2c138F46351D9E5FD26331B1aa4",
+ "constructorArgs": ["0x4242424242424242424242424242424242424242"]
+ }
+ },
+ "validatorsExitBusOracle": {
+ "deployParameters": {
+ "consensusVersion": 1
+ },
+ "proxy": {
+ "contract": "contracts/0.8.9/proxy/OssifiableProxy.sol",
+ "address": "0xffDDF7025410412deaa05E3E1cE68FE53208afcb",
+ "constructorArgs": [
+ "0x210f60EC8A4D020b3e22f15fee2d2364e9b22357",
+ "0x22896Bfc68814BFD855b1a167255eE497006e730",
+ "0x"
+ ]
+ },
+ "implementation": {
+ "contract": "contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol",
+ "address": "0x210f60EC8A4D020b3e22f15fee2d2364e9b22357",
+ "constructorArgs": [12, 1695902400, "0x28FAB2059C713A7F9D8c86Db49f9bb0e96Af1ef8"]
+ }
+ },
+ "vestingParams": {
+ "unvestedTokensAmount": "0",
+ "holders": {
+ "0xCD1f9954330AF39a74Fd6e7B25781B4c24ee373f": "880000000000000000000000",
+ "0xaa6bfBCD634EE744CB8FE522b29ADD23124593D3": "60000000000000000000000",
+ "0xBA59A84C6440E8cccfdb5448877E26F1A431Fc8B": "60000000000000000000000"
+ },
+ "start": 0,
+ "cliff": 0,
+ "end": 0,
+ "revokable": false
+ },
+ "withdrawalQueueERC721": {
+ "deployParameters": {
+ "name": "stETH Withdrawal NFT",
+ "symbol": "unstETH"
+ },
+ "proxy": {
+ "contract": "contracts/0.8.9/proxy/OssifiableProxy.sol",
+ "address": "0xc7cc160b58F8Bb0baC94b80847E2CF2800565C50",
+ "constructorArgs": [
+ "0xFF72B5cdc701E9eE677966B2702c766c38F412a4",
+ "0x22896Bfc68814BFD855b1a167255eE497006e730",
+ "0x"
+ ]
+ },
+ "implementation": {
+ "contract": "contracts/0.8.9/WithdrawalQueueERC721.sol",
+ "address": "0xFF72B5cdc701E9eE677966B2702c766c38F412a4",
+ "constructorArgs": ["0x8d09a4502Cc8Cf1547aD300E066060D043f6982D", "stETH Withdrawal NFT", "unstETH"]
+ }
+ },
+ "withdrawalVault": {
+ "implementation": {
+ "contract": "contracts/0.8.9/WithdrawalVault.sol",
+ "address": "0xd517d9d04DA9B47dA23df91261bd3bF435BE964A",
+ "constructorArgs": ["0x3F1c547b21f65e10480dE3ad8E19fAAC46C95034", "0xE92329EC7ddB11D25e25b3c21eeBf11f15eB325d"]
+ },
+ "proxy": {
+ "contract": "contracts/0.8.4/WithdrawalsManagerProxy.sol",
+ "address": "0xF0179dEC45a37423EAD4FaD5fCb136197872EAd9",
+ "constructorArgs": ["0xdA7d2573Df555002503F29aA4003e398d28cc00f", "0xd517d9d04DA9B47dA23df91261bd3bF435BE964A"]
+ }
+ },
+ "wstETH": {
+ "contract": "contracts/0.6.12/WstETH.sol",
+ "address": "0x8d09a4502Cc8Cf1547aD300E066060D043f6982D",
+ "constructorArgs": ["0x3F1c547b21f65e10480dE3ad8E19fAAC46C95034"]
+ }
+}
diff --git a/deployed-sepolia.json b/deployed-sepolia.json
index 2c1ac674e..510dcb92e 100644
--- a/deployed-sepolia.json
+++ b/deployed-sepolia.json
@@ -373,6 +373,7 @@
"0x7637d44c9f2e9cA584a8B5D2EA493012A5cdaEB6"
]
},
+ "initialDeployTotalGasUsed": "93822319027187012352012355612355612355610342610347112361112359910143001235561950934066901010464461610104644669101046446691060844875210608248680106082486801061274870010094944627101001446332871228712287122871228712",
"ldo": {
"address": "0xd06dF83b8ad6D89C86a187fba4Eae918d497BdCB",
"contract": "@aragon/minime/contracts/MiniMeToken.sol",
diff --git a/deployed-testnet-defaults.json b/deployed-testnet-defaults.json
new file mode 100644
index 000000000..bf00a06f2
--- /dev/null
+++ b/deployed-testnet-defaults.json
@@ -0,0 +1,127 @@
+{
+ "deployer": null,
+ "gateSeal": {
+ "factoryAddress": null,
+ "sealDuration": 518400,
+ "expiryTimestamp": 1714521600
+ },
+ "lidoApmEnsName": "lidopm.eth",
+ "lidoApmEnsRegDurationSec": 94608000,
+ "daoAragonId": "lido-dao",
+ "chainSpec": {
+ "slotsPerEpoch": 32,
+ "secondsPerSlot": 12,
+ "genesisTime": null,
+ "depositContract": null
+ },
+ "daoInitialSettings": {
+ "voting": {
+ "minSupportRequired": "500000000000000000",
+ "minAcceptanceQuorum": "50000000000000000",
+ "voteDuration": 900,
+ "objectionPhaseDuration": 300
+ },
+ "fee": {
+ "totalPercent": 10,
+ "treasuryPercent": 50,
+ "nodeOperatorsPercent": 50
+ },
+ "token": {
+ "name": "TEST Lido DAO Token",
+ "symbol": "TLDO"
+ }
+ },
+ "vestingParams": {
+ "unvestedTokensAmount": "0",
+ "holders": {
+ "0xCD1f9954330AF39a74Fd6e7B25781B4c24ee373f": "820000000000000000000000",
+ "0xaa6bfBCD634EE744CB8FE522b29ADD23124593D3": "60000000000000000000000",
+ "0xBA59A84C6440E8cccfdb5448877E26F1A431Fc8B": "60000000000000000000000",
+ "lido-aragon-agent-placeholder": "60000000000000000000000"
+ },
+ "start": 0,
+ "cliff": 0,
+ "end": 0,
+ "revokable": false
+ },
+ "burner": {
+ "deployParameters": {
+ "totalCoverSharesBurnt": "0",
+ "totalNonCoverSharesBurnt": "0"
+ }
+ },
+ "legacyOracle": {
+ "deployParameters": {
+ "lastCompletedEpochId": 0
+ }
+ },
+ "hashConsensusForAccountingOracle": {
+ "deployParameters": {
+ "fastLaneLengthSlots": 10,
+ "epochsPerFrame": 12
+ }
+ },
+ "accountingOracle": {
+ "deployParameters": {
+ "consensusVersion": 1
+ }
+ },
+ "hashConsensusForValidatorsExitBusOracle": {
+ "deployParameters": {
+ "fastLaneLengthSlots": 10,
+ "epochsPerFrame": 4
+ }
+ },
+ "validatorsExitBusOracle": {
+ "deployParameters": {
+ "consensusVersion": 1
+ }
+ },
+ "depositSecurityModule": {
+ "deployParameters": {
+ "maxDepositsPerBlock": 150,
+ "minDepositBlockDistance": 5,
+ "pauseIntentValidityPeriodBlocks": 6646,
+ "usePredefinedAddressInstead": null
+ }
+ },
+ "oracleReportSanityChecker": {
+ "deployParameters": {
+ "churnValidatorsPerDayLimit": 1500,
+ "oneOffCLBalanceDecreaseBPLimit": 500,
+ "annualBalanceIncreaseBPLimit": 1000,
+ "simulatedShareRateDeviationBPLimit": 250,
+ "maxValidatorExitRequestsPerReport": 2000,
+ "maxAccountingExtraDataListItemsCount": 100,
+ "maxNodeOperatorsPerExtraDataItemCount": 100,
+ "requestTimestampMargin": 128,
+ "maxPositiveTokenRebase": 5000000
+ }
+ },
+ "oracleDaemonConfig": {
+ "deployParameters": {
+ "NORMALIZED_CL_REWARD_PER_EPOCH": 64,
+ "NORMALIZED_CL_REWARD_MISTAKE_RATE_BP": 1000,
+ "REBASE_CHECK_NEAREST_EPOCH_DISTANCE": 1,
+ "REBASE_CHECK_DISTANT_EPOCH_DISTANCE": 23,
+ "VALIDATOR_DELAYED_TIMEOUT_IN_SLOTS": 7200,
+ "VALIDATOR_DELINQUENT_TIMEOUT_IN_SLOTS": 28800,
+ "NODE_OPERATOR_NETWORK_PENETRATION_THRESHOLD_BP": 100,
+ "PREDICTION_DURATION_IN_SLOTS": 50400,
+ "FINALIZATION_MAX_NEGATIVE_REBASE_EPOCH_SHIFT": 1350
+ }
+ },
+ "nodeOperatorsRegistry": {
+ "deployParameters": {
+ "stakingModuleTypeId": "curated-onchain-v1",
+ "stuckPenaltyDelay": 172800
+ }
+ },
+ "withdrawalQueueERC721": {
+ "deployParameters": {
+ "name": "Lido: stETH Withdrawal NFT",
+ "symbol": "unstETH",
+ "baseUri": null
+ }
+ }
+}
diff --git a/scripts/deploy-sepolia-deposit-contract-adapter.js b/scripts/deploy-sepolia-deposit-contract-adapter.js
new file mode 100644
index 000000000..e64572ebe
--- /dev/null
+++ b/scripts/deploy-sepolia-deposit-contract-adapter.js
@@ -0,0 +1,43 @@
+const runOrWrapScript = require('./helpers/run-or-wrap-script')
+const { deployBehindOssifiableProxy } = require('./helpers/deploy')
+
+const DEPLOYER = process.env.DEPLOYER || ''
+
+async function updateAdapterImplementation(proxyOwner) {
+ const sepoliaDepositAdapter = ""
+ const proxyAddress = "0x80b5DC88C98E528bF9cb4B7F0f076aC41da24651"
+
+ const OssifiableProxy = await artifacts.require('OssifiableProxy')
+ const proxy = await OssifiableProxy.at(proxyAddress)
+
+ await proxy.proxy__upgradeTo(sepoliaDepositAdapter, { from: proxyOwner })
+}
+
+async function deployAdaperBehindProxy(depositAdapterProxyOwner) {
+ const sepoliaDepositContract = "0x7f02C3E3c98b133055B8B348B2Ac625669Ed295D"
+ const constructorArgs = [sepoliaDepositContract]
+
+ const contract = await deployBehindOssifiableProxy(null, "SepoliaDepositAdapter", depositAdapterProxyOwner, DEPLOYER,
+ constructorArgs, null)
+
+ console.log('new-deposit-adapter-address', contract)
+}
+
+// RPC_URL= yarn hardhat --network sepolia verify --no-compile --contract "contracts/0.8.9/SepoliaDepositAdapter.sol:SepoliaDepositAdapter" --constructor-args contract-args.js
+// contract-args.js example:
+// module.exports = [
+// '0x7f02C3E3c98b133055B8B348B2Ac625669Ed295D'
+// ]
+
+async function deployNewContracts({ web3, artifacts }) {
+ if (!DEPLOYER) {
+ throw new Error('Deployer is not specified')
+ }
+
+ const depositAdapterProxyOwner = "0x6885E36BFcb68CB383DfE90023a462C03BCB2AE5"
+
+ // await deployAdaperBehindProxy(depositAdapterProxyOwner)
+ await updateAdapterImplementation(depositAdapterProxyOwner)
+}
+
+module.exports = runOrWrapScript(deployNewContracts, module)
diff --git a/scripts/scratch/00-populate-deploy-artifact-from-env.js b/scripts/scratch/00-populate-deploy-artifact-from-env.js
new file mode 100644
index 000000000..b0185e4f2
--- /dev/null
+++ b/scripts/scratch/00-populate-deploy-artifact-from-env.js
@@ -0,0 +1,59 @@
+const { ZERO_ADDRESS } = require('@aragon/contract-helpers-test')
+const runOrWrapScript = require('../helpers/run-or-wrap-script')
+const { readNetworkState, persistNetworkState } = require('../helpers/persisted-network-state')
+
+const DEPLOYER = process.env.DEPLOYER
+const GATE_SEAL_FACTORY = process.env.GATE_SEAL_FACTORY
+const GENESIS_TIME = parseInt(process.env.GENESIS_TIME)
+const DEPOSIT_CONTRACT = process.env.DEPOSIT_CONTRACT
+const WITHDRAWAL_QUEUE_BASE_URI = process.env.WITHDRAWAL_QUEUE_BASE_URI
+const DSM_PREDEFINED_ADDRESS = process.env.DSM_PREDEFINED_ADDRESS
+
+async function saveDeployParameters({ web3 }) {
+ const netId = await web3.eth.net.getId()
+
+ console.log('Using env values:')
+ console.log({
+ DEPLOYER,
+ GATE_SEAL_FACTORY,
+ GENESIS_TIME,
+ DEPOSIT_CONTRACT,
+ WITHDRAWAL_QUEUE_BASE_URI,
+ })
+
+ const state = readNetworkState(network.name, netId)
+ const ldoHolder = Object.keys(state.vestingParams.holders)[0]
+ const gateSealAddress = (GATE_SEAL_FACTORY === null || GATE_SEAL_FACTORY === ZERO_ADDRESS)
+ ? ZERO_ADDRESS : ''
+
+ state.networkId = await web3.eth.net.getId()
+ state.chainId = (await ethers.provider.getNetwork()).chainId
+ state.deployer = DEPLOYER
+ state.gateSeal = {
+ ...state.gateSeal,
+ factoryAddress: GATE_SEAL_FACTORY,
+ sealingCommittee: ldoHolder,
+ address: gateSealAddress,
+ }
+ state.chainSpec = {
+ ...state.chainSpec,
+ genesisTime: GENESIS_TIME,
+ depositContract: DEPOSIT_CONTRACT,
+ }
+ if (WITHDRAWAL_QUEUE_BASE_URI !== undefined) {
+ state.withdrawalQueueERC721.deployParameters = {
+ ...state.withdrawalQueueERC721.deployParameters,
+ baseUri: WITHDRAWAL_QUEUE_BASE_URI,
+ }
+ }
+ if (DSM_PREDEFINED_ADDRESS !== undefined) {
+ state.depositSecurityModule.deployParameters = {
+ ...state.depositSecurityModule.deployParameters,
+ usePredefinedAddressInstead: DSM_PREDEFINED_ADDRESS,
+ }
+ state.depositSecurityModule.address = DSM_PREDEFINED_ADDRESS
+ }
+ persistNetworkState(network.name, netId, state)
+}
+
+module.exports = runOrWrapScript(saveDeployParameters, module)
diff --git a/scripts/scratch/02-deploy-aragon-env.js b/scripts/scratch/02-deploy-aragon-env.js
new file mode 100644
index 000000000..19489fbce
--- /dev/null
+++ b/scripts/scratch/02-deploy-aragon-env.js
@@ -0,0 +1,327 @@
+const chalk = require('chalk')
+const namehash = require('eth-ens-namehash').hash
+const keccak256 = require('js-sha3').keccak_256
+const getAccounts = require('@aragon/os/scripts/helpers/get-accounts')
+
+const runOrWrapScript = require('../helpers/run-or-wrap-script')
+const { log, logSplitter, logWideSplitter, logHeader } = require('../helpers/log')
+const { deployImplementation, deployContract, makeTx, TotalGasCounter } = require('../helpers/deploy')
+const { readNetworkState, persistNetworkState } = require('../helpers/persisted-network-state')
+
+const { deployAPM } = require('../components/apm')
+const { assignENSName } = require('../components/ens')
+
+const ARAGON_ENS_LABEL = process.env.ARAGON_ENS_LABEL || 'aragonpm'
+const NETWORK_STATE_FILE = process.env.NETWORK_STATE_FILE || 'deployed.json'
+
+const ZERO_ADDR = '0x0000000000000000000000000000000000000000'
+
+async function deployAragonEnv({ web3, artifacts, networkStateFile = NETWORK_STATE_FILE }) {
+ const netId = await web3.eth.net.getId()
+ const accounts = await getAccounts(web3)
+
+ logWideSplitter()
+ log(`Network ID: ${chalk.yellow(netId)}`)
+
+ let state = readNetworkState(network.name, netId)
+
+ if (state.deployer) {
+ const lowercaseOwner = state.deployer.toLowerCase()
+ if (!accounts.some((acc) => acc.toLowerCase() === lowercaseOwner)) {
+ throw new Error(`owner account ${state.deployer} is missing from provided accounts`)
+ }
+ } else {
+ state.deployer = accounts[0]
+ log(`Setting owner to the first provided account: ${chalk.yellow(state.deployer)}`)
+ }
+
+ if (!state.aragonEnsLabelName) {
+ state.aragonEnsLabelName = ARAGON_ENS_LABEL
+ log(`Using Aragon ENS label: ${state.aragonEnsLabelName}`)
+ }
+ persistNetworkState(network.name, netId, state)
+
+ logHeader(`ENS`)
+ const { ens, ensFactory } = await useOrDeployENS({
+ artifacts,
+ owner: state.deployer,
+ ensAddress: state.ensAddress,
+ })
+ state = readNetworkState(network.name, netId)
+ state.ens = {
+ address: ens.address,
+ constructorArgs: ens.constructorArgs,
+ }
+ state.ensFactory = {
+ address: ensFactory.address,
+ constructorArgs: ensFactory.constructorArgs,
+ }
+ persistNetworkState(network.name, netId, state)
+
+ logHeader(`DAO factory`)
+ const { daoFactory, evmScriptRegistryFactory } = await useOrDeployDaoFactory({
+ artifacts,
+ owner: state.deployer,
+ daoFactoryAddress: state.daoFactoryAddress
+ })
+ state = readNetworkState(network.name, netId)
+ state.daoFactory = {
+ address: daoFactory.address,
+ constructorArgs: daoFactory.constructorArgs,
+ }
+ state.evmScriptRegistryFactory = {
+ address: evmScriptRegistryFactory.address,
+ constructorArgs: evmScriptRegistryFactory.constructorArgs,
+ }
+ persistNetworkState(network.name, netId, state)
+
+ logHeader(`APM registry factory`)
+ const {
+ apmRegistryBase,
+ apmRepoBase,
+ ensSubdomainRegistrarBase,
+ apmRegistryFactory
+ } = await useOrDeployAPMRegistryFactory({
+ artifacts,
+ owner: state.deployer,
+ ens: ens,
+ daoFactory: daoFactory,
+ apmRegistryFactoryAddress: state.apmRegistryFactoryAddress,
+ apmRegistryBaseAddress: state.apmRegistryBaseAddress,
+ apmRepoBaseAddress: state.apmRepoBaseAddress,
+ ensSubdomainRegistrarBaseAddress: state.ensSubdomainRegistrarBaseAddress,
+ })
+ state = readNetworkState(network.name, netId)
+ state.apmRegistry = {
+ implementation: {
+ address: apmRegistryBase,
+ },
+ }
+ state.apmRepo = {
+ implementation: {
+ address: apmRepoBase.address,
+ constructorArgs: apmRepoBase.constructorArgs,
+ },
+ }
+ state.ensSubdomainRegistrar = {
+ implementation: {
+ address: ensSubdomainRegistrarBase.address,
+ constructorArgs: ensSubdomainRegistrarBase.constructorArgs,
+ },
+ }
+ state.apmRegistryFactory = {
+ address: apmRegistryFactory.address,
+ constructorArgs: apmRegistryFactory.constructorArgs,
+ }
+ persistNetworkState(network.name, netId, state)
+
+ logHeader(`Aragon APM`)
+ const {
+ apmRegistry,
+ ensNodeName,
+ ensNode,
+ } = await deployAPM({
+ web3,
+ artifacts,
+ owner: state.deployer,
+ labelName: state.aragonEnsLabelName,
+ ens: ens,
+ apmRegistryFactory: apmRegistryFactory,
+ apmRegistryAddress: state.aragonApmRegistryAddress
+ })
+ state = readNetworkState(network.name, netId)
+ state.ensNode = {
+ nodeName: ensNodeName,
+ nodeId: ensNode,
+ }
+ state.apmRegistry = {
+ proxy: {
+ address: apmRegistry.address,
+ },
+ }
+ persistNetworkState(network.name, netId, state)
+
+ logHeader(`MiniMeTokenFactory`)
+ const { miniMeTokenFactory } = await deployMiniMeTokenFactory({
+ artifacts,
+ owner: state.deployer,
+ miniMeTokenFactoryAddress: state.miniMeTokenFactoryAddress
+ })
+ state = readNetworkState(network.name, netId)
+ state.miniMeTokenFactory = {
+ address: miniMeTokenFactory.address,
+ constructorArgs: miniMeTokenFactory.constructorArgs,
+ }
+ persistNetworkState(network.name, netId, state)
+
+ logHeader('AragonID')
+ const { aragonID } = await deployAragonID({
+ artifacts,
+ owner: state.deployer,
+ ens: ens,
+ aragonIDAddress: state.aragonIDAddress
+ })
+ state = readNetworkState(network.name, netId)
+ state.aragonID = {
+ address: aragonID.address,
+ constructorArgs: aragonID.constructorArgs,
+ }
+ persistNetworkState(network.name, netId, state)
+
+ await TotalGasCounter.incrementTotalGasUsedInStateFile()
+}
+
+async function useOrDeployENS({ artifacts, owner, ensAddress }) {
+ if (!ensAddress) {
+ return await deployENS({ artifacts, owner })
+ } else {
+ const ENS = artifacts.require('ENS')
+ logSplitter()
+ log(`Using ENS: ${chalk.yellow(ensAddress)}`)
+ return {
+ ens: await ENS.at(ensAddress)
+ }
+ }
+}
+
+async function deployENS({ artifacts, owner }) {
+ const ENS = artifacts.require('ENS')
+
+ const factoryAddress = (await deployContract('ENSFactory', [], owner)).address
+ const factory = await artifacts.require('ENSFactory').at(factoryAddress)
+ const result = await makeTx(factory, 'newENS', [owner], { from: owner })
+
+ const ensAddr = result.logs.filter((l) => l.event === 'DeployENS')[0].args.ens
+ log(`ENS address: ${chalk.yellow(ensAddr)}`)
+
+ return {
+ ens: await ENS.at(ensAddr),
+ ensFactory: factory
+ }
+}
+
+async function useOrDeployDaoFactory({ artifacts, owner, daoFactoryAddress }) {
+ let daoFactory
+ if (daoFactoryAddress) {
+ daoFactory = await artifacts.require('DAOFactory').at(daoFactoryAddress)
+ const hasEVMScripts = (await daoFactory.regFactory()) !== ZERO_ADDR
+ log(`Using DAOFactory (with${hasEVMScripts ? '' : 'out'} EVMScripts): ${chalk.yellow(daoFactoryAddress)}`)
+ return { daoFactory }
+ } else {
+ log(`Deploying DAOFactory with EVMScripts...`)
+ return await deployDAOFactory({ artifacts, owner, withEvmScriptRegistryFactory: true })
+ }
+}
+
+async function useOrDeployAPMRegistryFactory({
+ artifacts,
+ owner,
+ ens,
+ daoFactory,
+ apmRegistryFactoryAddress,
+ apmRegistryBaseAddress,
+ apmRepoBaseAddress,
+ ensSubdomainRegistrarBaseAddress
+}) {
+ if (!apmRegistryBaseAddress) {
+ apmRegistryBaseAddress = (await deployContract('APMRegistry', [], owner)).address
+ }
+ const apmRegistryBase = await artifacts.require('APMRegistry').at(apmRegistryBaseAddress)
+
+ if (!apmRepoBaseAddress) {
+ apmRepoBaseAddress = (await deployContract('Repo', [], owner)).address
+ }
+ const apmRepoBase = await artifacts.require('Repo').at(apmRepoBaseAddress)
+
+ if (!ensSubdomainRegistrarBaseAddress) {
+ ensSubdomainRegistrarBaseAddress = (await deployContract('ENSSubdomainRegistrar', [], owner)).address
+ }
+ const ensSubdomainRegistrarBase = await artifacts.require('ENSSubdomainRegistrar').at(ensSubdomainRegistrarBaseAddress)
+
+ const apmRegistryFactoryArgs = [
+ daoFactory.address, apmRegistryBase.address, apmRepoBase.address, ensSubdomainRegistrarBase.address, ens.address, ZERO_ADDR
+ ]
+ if (!apmRegistryFactoryAddress) {
+ apmRegistryFactoryAddress = (await deployContract('APMRegistryFactory', apmRegistryFactoryArgs, owner)).address
+ }
+ const apmRegistryFactory = await artifacts.require('APMRegistryFactory').at(apmRegistryFactoryAddress)
+
+ return { apmRegistryBase, apmRepoBase, ensSubdomainRegistrarBase, apmRegistryFactory }
+}
+
+async function deployDAOFactory({ artifacts, owner, kernelBaseAddress, aclBaseAddress, withEvmScriptRegistryFactory }) {
+ const kernelBase = await deployImplementation('aragon-kernel', 'Kernel', owner, [true])
+
+ const aclBase = await deployImplementation('aragon-acl', 'ACL', owner)
+
+ let evmScriptRegistryFactory = undefined
+ if (withEvmScriptRegistryFactory) {
+ const evmScriptRegistryFactoryAddress = (await deployContract('EVMScriptRegistryFactory', [], owner)).address
+ evmScriptRegistryFactory = await artifacts.require('EVMScriptRegistryFactory').at(evmScriptRegistryFactoryAddress)
+ }
+
+ const daoFactoryArgs = [
+ kernelBase.address,
+ aclBase.address,
+ evmScriptRegistryFactory ? evmScriptRegistryFactory.address : ZERO_ADDR
+ ]
+ const daoFactoryAddress = (await deployContract('DAOFactory', daoFactoryArgs, owner)).address
+ const daoFactory = await artifacts.require('DAOFactory').at(daoFactoryAddress)
+
+ return {
+ kernelBase,
+ aclBase,
+ ...(evmScriptRegistryFactory ? { evmScriptRegistryFactory } : null),
+ daoFactory,
+ }
+}
+
+async function deployMiniMeTokenFactory({ artifacts, owner, miniMeTokenFactoryAddress }) {
+ if (!miniMeTokenFactoryAddress) {
+ miniMeTokenFactoryAddress = (await deployContract('MiniMeTokenFactory', [], owner)).address
+ }
+ const factory = await artifacts.require('MiniMeTokenFactory').at(miniMeTokenFactoryAddress)
+
+ return { miniMeTokenFactory: factory }
+}
+
+async function deployAragonID({ artifacts, owner, ens, aragonIDAddress }) {
+ const FIFSResolvingRegistrar = artifacts.require('FIFSResolvingRegistrar')
+ if (aragonIDAddress != null) {
+ log(`Using FIFSResolvingRegistrar: ${chalk.yellow(aragonIDAddress)}`)
+ return {
+ aragonID: await FIFSResolvingRegistrar.at(aragonIDAddress)
+ }
+ }
+
+ const publicNode = namehash('resolver.eth')
+ const publicResolverAddress = await ens.resolver(publicNode)
+ log(`Using public resolver: ${chalk.yellow(publicResolverAddress)}`)
+
+ const nodeName = 'aragonid.eth'
+ const node = namehash(nodeName)
+ log(`ENS node: ${chalk.yellow(nodeName)} (${node})`)
+
+ const fifsResolvingRegistrarArgs = [
+ ens.address, publicResolverAddress, node
+ ]
+ const registrarAddress = (await deployContract('FIFSResolvingRegistrar', fifsResolvingRegistrarArgs, owner)).address
+ const aragonID = await artifacts.require('FIFSResolvingRegistrar').at(registrarAddress)
+
+ logSplitter()
+ await assignENSName({
+ parentName: 'eth',
+ labelName: 'aragonid',
+ assigneeAddress: aragonID.address,
+ assigneeDesc: 'AragonID',
+ owner,
+ ens
+ })
+
+ logSplitter()
+ await makeTx(aragonID, 'register', ['0x' + keccak256('owner'), owner], { from: owner })
+
+ return { aragonID, aragonIDEnsNodeName: nodeName, aragonIDEnsNode: node }
+}
+
+module.exports = runOrWrapScript(deployAragonEnv, module)
diff --git a/scripts/scratch/03-deploy-aragon-std-apps.js b/scripts/scratch/03-deploy-aragon-std-apps.js
new file mode 100644
index 000000000..33534cc5c
--- /dev/null
+++ b/scripts/scratch/03-deploy-aragon-std-apps.js
@@ -0,0 +1,24 @@
+const runOrWrapScript = require('../helpers/run-or-wrap-script')
+const { readNetworkState, assertRequiredNetworkState } = require('../helpers/persisted-network-state')
+const { deployImplementation, TotalGasCounter } = require('../helpers/deploy')
+
+const REQUIRED_NET_STATE = [
+ 'deployer',
+]
+
+async function deployAragonStdApps({ web3, artifacts, }) {
+ const netId = await web3.eth.net.getId()
+ const state = readNetworkState(network.name, netId)
+ assertRequiredNetworkState(state, REQUIRED_NET_STATE)
+
+ const deployer = state.deployer
+ await deployImplementation("app:aragon-agent", "Agent", deployer)
+ await deployImplementation("app:aragon-finance", "Finance", deployer)
+ await deployImplementation("app:aragon-token-manager", "TokenManager", deployer)
+ await deployImplementation("app:aragon-voting", "Voting", deployer)
+
+ await TotalGasCounter.incrementTotalGasUsedInStateFile()
+}
+
+
+module.exports = runOrWrapScript(deployAragonStdApps, module)
diff --git a/scripts/scratch/04-deploy-lido-template-and-bases.js b/scripts/scratch/04-deploy-lido-template-and-bases.js
new file mode 100644
index 000000000..3150492bd
--- /dev/null
+++ b/scripts/scratch/04-deploy-lido-template-and-bases.js
@@ -0,0 +1,58 @@
+const chalk = require('chalk')
+
+const runOrWrapScript = require('../helpers/run-or-wrap-script')
+const { log } = require('../helpers/log')
+const { deployImplementation, deployWithoutProxy, TotalGasCounter } = require('../helpers/deploy')
+const { readNetworkState, assertRequiredNetworkState, persistNetworkState } = require('../helpers/persisted-network-state')
+const { APP_NAMES } = require('../constants')
+
+const REQUIRED_NET_STATE = [
+ 'ens',
+ 'daoFactory',
+ 'miniMeTokenFactory',
+ 'aragonID',
+ 'apmRegistryFactory',
+ 'deployer'
+]
+
+async function deployTemplate({ web3, artifacts }) {
+ const netId = await web3.eth.net.getId()
+
+ log.splitter()
+ log(`Network ID: ${chalk.yellow(netId)}`)
+
+ const state = readNetworkState(network.name, netId)
+ assertRequiredNetworkState(state, REQUIRED_NET_STATE)
+
+ const daoTemplateConstructorArgs = [
+ state.deployer,
+ state.daoFactory.address,
+ state.ens.address,
+ state.miniMeTokenFactory.address,
+ state.aragonID.address,
+ state.apmRegistryFactory.address
+ ]
+
+ log.splitter()
+
+ await deployWithoutProxy('lidoTemplate', 'LidoTemplate', state.deployer, daoTemplateConstructorArgs)
+ const daoTemplateDeployBlock = (await ethers.provider.getBlock('latest')).number
+
+ await deployImplementation(`app:${APP_NAMES.LIDO}`, 'Lido', state.deployer)
+
+ await deployImplementation(`app:${APP_NAMES.ORACLE}`, 'LegacyOracle', state.deployer)
+
+ await deployImplementation(`app:${APP_NAMES.NODE_OPERATORS_REGISTRY}`, 'NodeOperatorsRegistry', state.deployer)
+
+ persistNetworkState(network.name, netId, readNetworkState(network.name, netId), {
+ lidoTemplate: {
+ deployBlock: daoTemplateDeployBlock,
+ }
+ })
+
+ log.splitter()
+
+ await TotalGasCounter.incrementTotalGasUsedInStateFile()
+}
+
+module.exports = runOrWrapScript(deployTemplate, module)
diff --git a/scripts/scratch/09-create-app-repos.js b/scripts/scratch/09-create-app-repos.js
new file mode 100644
index 000000000..8e632c6b8
--- /dev/null
+++ b/scripts/scratch/09-create-app-repos.js
@@ -0,0 +1,87 @@
+const chalk = require('chalk')
+const { assert } = require('chai')
+
+const runOrWrapScript = require('../helpers/run-or-wrap-script')
+const { log, logSplitter, logWideSplitter } = require('../helpers/log')
+const { assertLastEvent } = require('../helpers/events')
+const { readNetworkState, assertRequiredNetworkState, persistNetworkState } = require('../helpers/persisted-network-state')
+const { makeTx, TotalGasCounter } = require('../helpers/deploy')
+
+const { APP_NAMES } = require('../constants')
+
+const NULL_CONTENT_URI = "0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
+
+
+const REQUIRED_NET_STATE = [
+ 'deployer',
+ 'lidoTemplate',
+ `app:${APP_NAMES.LIDO}`,
+ `app:${APP_NAMES.ORACLE}`,
+ `app:${APP_NAMES.NODE_OPERATORS_REGISTRY}`,
+ `app:${APP_NAMES.ARAGON_AGENT}`,
+ `app:${APP_NAMES.ARAGON_FINANCE}`,
+ `app:${APP_NAMES.ARAGON_TOKEN_MANAGER}`,
+ `app:${APP_NAMES.ARAGON_VOTING}`,
+]
+
+async function createAppRepos({ web3, artifacts }) {
+ const netId = await web3.eth.net.getId()
+
+ logWideSplitter()
+ log(`Network ID: ${chalk.yellow(netId)}`)
+
+ const state = readNetworkState(network.name, netId)
+ assertRequiredNetworkState(state, REQUIRED_NET_STATE)
+ const daoTemplateAddress = state.lidoTemplate.address
+
+ logSplitter()
+ log(`Using LidoTemplate: ${chalk.yellow(daoTemplateAddress)}`)
+ const template = await artifacts.require('LidoTemplate').at(daoTemplateAddress)
+ if (state.lidoTemplate.deployBlock) {
+ log(`Using LidoTemplate deploy block: ${chalk.yellow(state.lidoTemplate.deployBlock)}`)
+ }
+
+ await assertLastEvent(template, 'TmplAPMDeployed', null, state.lidoTemplate.deployBlock)
+ logSplitter()
+
+ const lidoAppState = state[`app:${APP_NAMES.LIDO}`]
+ const oracleAppState = state[`app:${APP_NAMES.ORACLE}`]
+ const nodeOperatorsAppState = state[`app:${APP_NAMES.NODE_OPERATORS_REGISTRY}`]
+
+ const createReposArguments = [
+ [1, 0, 0],
+ // Lido app
+ lidoAppState.implementation.address,
+ NULL_CONTENT_URI,
+ // NodeOperatorsRegistry app
+ nodeOperatorsAppState.implementation.address,
+ NULL_CONTENT_URI,
+ // LegacyOracle app
+ oracleAppState.implementation.address,
+ NULL_CONTENT_URI,
+ ]
+ const from = state.deployer
+
+ console.log({arguments, from})
+
+ const lidoAppsReceipt = await makeTx(template, 'createRepos', createReposArguments, { from })
+ console.log(`=== Aragon Lido Apps Repos (Lido, AccountingOracle, NodeOperatorsRegistry deployed: ${lidoAppsReceipt.tx} ===`)
+
+ const createStdAragonReposArguments = [
+ state['app:aragon-agent'].implementation.address,
+ state['app:aragon-finance'].implementation.address,
+ state['app:aragon-token-manager'].implementation.address,
+ state['app:aragon-voting'].implementation.address,
+ ]
+
+ const aragonStdAppsReceipt = await makeTx(template, 'createStdAragonRepos', createStdAragonReposArguments, { from })
+ console.log(`=== Aragon Std Apps Repos (Agent, Finance, TokenManager, Voting deployed: ${aragonStdAppsReceipt.tx} ===`)
+ state.lidoTemplateCreateStdAppReposTx = aragonStdAppsReceipt.tx
+
+ logSplitter()
+ persistNetworkState(network.name, netId, state)
+
+ await TotalGasCounter.incrementTotalGasUsedInStateFile()
+}
+
+module.exports = runOrWrapScript(createAppRepos, module)
diff --git a/scripts/scratch/11-obtain-deployed-dao.js b/scripts/scratch/11-obtain-deployed-dao.js
new file mode 100644
index 000000000..740851886
--- /dev/null
+++ b/scripts/scratch/11-obtain-deployed-dao.js
@@ -0,0 +1,207 @@
+const path = require('path')
+const chalk = require('chalk')
+const { hash: namehash } = require('eth-ens-namehash')
+const { toChecksumAddress } = require('web3-utils')
+const { getEventArgument, getEvents, ZERO_ADDRESS } = require('@aragon/contract-helpers-test')
+
+const runOrWrapScript = require('../helpers/run-or-wrap-script')
+const { log } = require('../helpers/log')
+const { assertLastEvent } = require('../helpers/events')
+const { getContractPath } = require('../helpers/deploy')
+const { readNetworkState, persistNetworkState, assertRequiredNetworkState } = require('../helpers/persisted-network-state')
+
+const { assertInstalledApps } = require('./checks/apps')
+const { APP_NAMES } = require('../constants')
+
+const REQUIRED_NET_STATE = [
+ 'lidoApmEnsName',
+ 'lidoTemplate',
+ `app:${APP_NAMES.LIDO}`,
+ `app:${APP_NAMES.ORACLE}`,
+ `app:${APP_NAMES.NODE_OPERATORS_REGISTRY}`
+]
+
+const VALID_APP_NAMES = Object.entries(APP_NAMES).map((e) => e[1])
+const AGENT_VESTING_PLACEHOLDER = 'lido-aragon-agent-placeholder'
+
+// See KernelConstants.sol
+const KERNEL_DEFAULT_ACL_APP_ID = '0xe3262375f45a6e2026b7e7b18c2b807434f2508fe1a2a3dfb493c7df8f4aad6a'
+
+function updateAgentVestingAddressPlaceholder(state) {
+ if (state['app:aragon-agent']) {
+ const agentAddress = state['app:aragon-agent'].proxy.address
+ const vestingAmount = state.vestingParams.holders[AGENT_VESTING_PLACEHOLDER]
+ state.vestingParams.holders[agentAddress] = vestingAmount
+ delete state.vestingParams.holders[AGENT_VESTING_PLACEHOLDER]
+ }
+}
+
+
+async function obtainDeployedAPM({ web3, artifacts }) {
+ const netId = await web3.eth.net.getId()
+
+ log.wideSplitter()
+ log(`Network ID: ${chalk.yellow(netId)}`)
+
+ const state = readNetworkState(network.name, netId)
+ assertRequiredNetworkState(state, REQUIRED_NET_STATE)
+ const daoTemplateAddress = state.lidoTemplate.address
+
+ log.splitter()
+
+
+ log(`Using LidoTemplate: ${chalk.yellow(daoTemplateAddress)}`)
+ const template = await artifacts.require('LidoTemplate').at(daoTemplateAddress)
+ if (state.lidoTemplate.deployBlock) {
+ log(`Using LidoTemplate deploy block: ${chalk.yellow(state.lidoTemplate.deployBlock)}`)
+ }
+ const daoDeployedEvt = await assertLastEvent(template, 'TmplDAOAndTokenDeployed', null, state.lidoTemplate.deployBlock)
+
+ const lidoApmEnsName = state.lidoApmEnsName
+ const appIdNameEntries = VALID_APP_NAMES.map((name) => [namehash(`${name}.${lidoApmEnsName}`), name])
+ const appNameByAppId = Object.fromEntries(appIdNameEntries)
+
+ const fromBlock = state.lidoTemplate.deployBlock
+ const appInstalledEvents = (await template.getPastEvents('TmplAppInstalled', { fromBlock })).map((evt) => evt.args)
+ for (const evt of appInstalledEvents) {
+ const appName = appNameByAppId[evt.appId]
+ const proxyAddress = toChecksumAddress(evt.appProxy)
+ console.log(`${appName}: ${proxyAddress} ${evt.appId} ${evt.initializeData}`)
+ }
+
+ state.newDaoTx = daoDeployedEvt.transactionHash
+ log(`Using newDao transaction: ${chalk.yellow(state.newDaoTx)}`)
+ persistNetworkState(network.name, netId, state)
+
+
+ log.splitter()
+
+ log(`Using Kernel: ${chalk.yellow(daoDeployedEvt.args.dao)}`)
+ const kernelProxyAddress = daoDeployedEvt.args.dao
+ const dao = await artifacts.require('Kernel').at(kernelProxyAddress)
+
+ log(`Using MiniMeToken: ${chalk.yellow(daoDeployedEvt.args.token)}`)
+ const daoToken = await artifacts.require('MiniMeToken').at(daoDeployedEvt.args.token)
+
+ log.splitter()
+
+ state['aragon-kernel'] = {
+ ...state['aragon-kernel'],
+ proxy: {
+ address: kernelProxyAddress,
+ contract: await getContractPath('KernelProxy'),
+ constructorArgs: [ // see DAOFactory.newDAO
+ state['aragon-kernel'].implementation.address,
+ ],
+ },
+ }
+
+ state.ldo = {
+ ...state.ldo,
+ address: daoToken.address,
+ contract: await getContractPath('MiniMeToken'),
+ constructorArgs: [ // see LidoTemplate._createToken
+ state.miniMeTokenFactory.address,
+ ZERO_ADDRESS,
+ 0,
+ state.daoInitialSettings.token.name,
+ 18, // see LidoTemplate.TOKEN_DECIMALS
+ state.daoInitialSettings.token.symbol,
+ true,
+ ],
+ }
+
+ const evmScriptRegistryFactory = await artifacts.require('EVMScriptRegistryFactory').at(state.evmScriptRegistryFactory.address)
+ state.callsScript = {
+ address: await evmScriptRegistryFactory.baseCallScript(),
+ contract: await getContractPath('CallsScript'),
+ constructorArgs: [], // see EVMScriptRegistryFactory.baseCallScript
+ }
+
+ const dataByAppName = await assertInstalledApps(
+ {
+ template,
+ dao,
+ lidoApmEnsName: state.lidoApmEnsName,
+ appProxyUpgradeableArtifactName: 'external:AppProxyUpgradeable_DAO'
+ },
+ state.lidoTemplate.deployBlock
+ )
+
+ for (const [appName, appData] of Object.entries(dataByAppName)) {
+ const key = `app:${appName}`
+ const proxyAddress = appData.proxyAddress
+ const initializeData = appData.initializeData
+ delete appData.proxyAddress
+ delete appData.initializeData
+ state[key] = {
+ ...state[key],
+ aragonApp: appData,
+ proxy: {
+ address: proxyAddress,
+ contract: await getContractPath('AppProxyUpgradeable'),
+ constructorArgs: [ // see AppProxyFactory
+ kernelProxyAddress,
+ appData.id,
+ initializeData,
+ ],
+ }
+ }
+ }
+ updateAgentVestingAddressPlaceholder(state)
+ log.splitter()
+ persistNetworkState(network.name, netId, state)
+
+ const newDaoReceipt = await web3.eth.getTransactionReceipt(state.lidoTemplateNewDaoTx)
+ const { abi: DAOFactoryABI } = await artifacts.readArtifact('DAOFactory')
+ const evmScriptRegistryEvents = getEvents(newDaoReceipt, 'DeployEVMScriptRegistry', { decodeForAbi: DAOFactoryABI })
+ const evmScriptRegistryAddress = evmScriptRegistryEvents[0].args.reg
+
+
+ // Get missing proxies
+ const { abi: KernelABI } = await artifacts.readArtifact('Kernel')
+ const newAppProxyEvents = getEvents(newDaoReceipt, 'NewAppProxy', { decodeForAbi: KernelABI })
+ for (const e of newAppProxyEvents) {
+ const appId = e.args.appId
+ if (appNameByAppId[appId] !== undefined) continue
+
+ let proxyContract, appName
+
+ if (appId == KERNEL_DEFAULT_ACL_APP_ID) {
+ proxyContract = 'AppProxyUpgradeable'
+ appName = 'aragon-acl'
+ } else { // otherwise it is EvmScriptRegistry
+ proxyContract = 'AppProxyPinned'
+ appName = 'aragon-evm-script-registry'
+ }
+
+ const proxy = await artifacts.require(proxyContract).at(e.args.proxy)
+
+ state[appName] = {
+ ...state[appName],
+ proxy: {
+ address: proxy.address,
+ constructorArgs: [ // See Kernel.initialize
+ kernelProxyAddress,
+ appId,
+ '0x00',
+ ],
+ contract: await getContractPath(proxyContract),
+ },
+ aragonApp: {
+ name: appName,
+ id: appId,
+ }
+ }
+ if (appName === 'aragon-evm-script-registry') {
+ state[appName].implementation = {
+ address: await proxy.implementation(),
+ contract: await getContractPath('EVMScriptRegistry'),
+ constructorArgs: [], // see DAOFactory.newDAO and EVMScriptRegistryFactory.baseReg
+ }
+ }
+ }
+ persistNetworkState(network.name, netId, state)
+}
+
+module.exports = runOrWrapScript(obtainDeployedAPM, module)
diff --git a/scripts/scratch/14-gate-seal.js b/scripts/scratch/14-gate-seal.js
new file mode 100644
index 000000000..0c92a42b8
--- /dev/null
+++ b/scripts/scratch/14-gate-seal.js
@@ -0,0 +1,51 @@
+const runOrWrapScript = require('../helpers/run-or-wrap-script')
+const { log, logSplitter, logWideSplitter, yl, gr } = require('../helpers/log')
+const { readNetworkState, assertRequiredNetworkState, persistNetworkState } = require('../helpers/persisted-network-state')
+const { getEventArgument } = require('@aragon/contract-helpers-test')
+const { makeTx, TotalGasCounter } = require('../helpers/deploy')
+
+const { APP_NAMES } = require('../constants')
+
+const REQUIRED_NET_STATE = [
+ "deployer",
+ "gateSeal",
+ "validatorsExitBusOracle",
+ "withdrawalQueueERC721",
+]
+
+async function deployNewContracts({ web3, artifacts }) {
+ const netId = await web3.eth.net.getId()
+ logWideSplitter()
+ log(`Network ID:`, yl(netId))
+ let state = readNetworkState(network.name, netId)
+ assertRequiredNetworkState(state, REQUIRED_NET_STATE)
+
+ if (state.gateSeal.address !== "") {
+ console.log(`Using the specified GateSeal address ${state.gateSeal.address}`)
+ return
+ }
+
+ const gateSealFactoryAddress = state.gateSeal.factoryAddress
+ const deployer = state.deployer
+ const sealables = [
+ state.withdrawalQueueERC721.proxy.address,
+ state.validatorsExitBusOracle.proxy.address,
+ ]
+
+ const GateSealFactory = await artifacts.require("IGateSealFactory")
+ const gateSealFactory = await GateSealFactory.at(gateSealFactoryAddress)
+ const receipt = await makeTx(gateSealFactory, "create_gate_seal", [
+ state.gateSeal.sealingCommittee,
+ state.gateSeal.sealDuration,
+ sealables,
+ state.gateSeal.expiryTimestamp,
+ ], { from: deployer })
+ const gateSealAddress = await getEventArgument(receipt, 'GateSealCreated', 'gate_seal')
+ console.log(`GateSeal created: ${gateSealAddress}`)
+ state.gateSeal.address = gateSealAddress
+ persistNetworkState(network.name, netId, state)
+
+ await TotalGasCounter.incrementTotalGasUsedInStateFile()
+}
+
+module.exports = runOrWrapScript(deployNewContracts, module)
diff --git a/scripts/scratch/17-grant-roles.js b/scripts/scratch/17-grant-roles.js
new file mode 100644
index 000000000..c7b2b40eb
--- /dev/null
+++ b/scripts/scratch/17-grant-roles.js
@@ -0,0 +1,87 @@
+const runOrWrapScript = require('../helpers/run-or-wrap-script')
+const { log, logSplitter, logWideSplitter, yl, gr } = require('../helpers/log')
+const { readNetworkState, assertRequiredNetworkState } = require('../helpers/persisted-network-state')
+
+const { APP_NAMES } = require('../constants')
+const { makeTx, TotalGasCounter } = require('../helpers/deploy')
+
+
+const REQUIRED_NET_STATE = [
+ `app:${APP_NAMES.LIDO}`,
+ `app:${APP_NAMES.ORACLE}`,
+ "app:aragon-agent",
+ "app:aragon-voting",
+ "app:node-operators-registry",
+ "accountingOracle",
+ "burner",
+ "daoInitialSettings",
+ "eip712StETH",
+ "hashConsensusForAccountingOracle",
+ "hashConsensusForValidatorsExitBusOracle",
+ "lidoLocator",
+ "stakingRouter",
+ "validatorsExitBusOracle",
+ "withdrawalQueueERC721",
+ "withdrawalVault",
+ "gateSeal",
+]
+
+
+async function deployNewContracts({ web3, artifacts }) {
+ const netId = await web3.eth.net.getId()
+ logWideSplitter()
+ log(`Network ID:`, yl(netId))
+
+ let state = readNetworkState(network.name, netId)
+ assertRequiredNetworkState(state, REQUIRED_NET_STATE)
+
+ const lidoAddress = state["app:lido"].proxy.address
+ const nodeOperatorsRegistryAddress = state["app:node-operators-registry"].proxy.address
+ const gateSealAddress = state.gateSeal.address
+
+ const burnerAddress = state["burner"].address
+ const stakingRouterAddress = state["stakingRouter"].proxy.address
+ const withdrawalQueueAddress = state["withdrawalQueueERC721"].proxy.address
+ const accountingOracleAddress = state["accountingOracle"].proxy.address
+ const validatorsExitBusOracleAddress = state["validatorsExitBusOracle"].proxy.address
+ const depositSecurityModuleAddress = state.depositSecurityModule.address
+
+ const deployer = state.deployer
+
+ //
+ // === StakingRouter
+ //
+ const stakingRouter = await artifacts.require('StakingRouter').at(stakingRouterAddress)
+ await makeTx(stakingRouter, 'grantRole', [await stakingRouter.STAKING_MODULE_PAUSE_ROLE(), depositSecurityModuleAddress], { from: deployer })
+ await makeTx(stakingRouter, 'grantRole', [await stakingRouter.STAKING_MODULE_RESUME_ROLE(), depositSecurityModuleAddress], { from: deployer })
+ await makeTx(stakingRouter, 'grantRole', [await stakingRouter.REPORT_EXITED_VALIDATORS_ROLE(), accountingOracleAddress], { from: deployer })
+ await makeTx(stakingRouter, 'grantRole', [await stakingRouter.REPORT_REWARDS_MINTED_ROLE(), lidoAddress], { from: deployer })
+ logWideSplitter()
+
+ //
+ // === ValidatorsExitBusOracle
+ //
+ const validatorsExitBusOracle = await artifacts.require('ValidatorsExitBusOracle').at(validatorsExitBusOracleAddress)
+ await makeTx(validatorsExitBusOracle, 'grantRole', [await validatorsExitBusOracle.PAUSE_ROLE(), gateSealAddress], { from: deployer })
+ logWideSplitter()
+
+ //
+ // === WithdrawalQueue
+ //
+ const withdrawalQueue = await artifacts.require('WithdrawalQueueERC721').at(withdrawalQueueAddress)
+ await makeTx(withdrawalQueue, 'grantRole', [await withdrawalQueue.PAUSE_ROLE(), gateSealAddress], { from: deployer })
+ await makeTx(withdrawalQueue, 'grantRole', [await withdrawalQueue.FINALIZE_ROLE(), lidoAddress], { from: deployer })
+ await makeTx(withdrawalQueue, 'grantRole', [await withdrawalQueue.ORACLE_ROLE(), accountingOracleAddress], { from: deployer })
+ logWideSplitter()
+
+ //
+ // === Burner
+ //
+ const burner = await artifacts.require('Burner').at(burnerAddress)
+ // NB: REQUEST_BURN_SHARES_ROLE is already granted to Lido in Burner constructor
+ await makeTx(burner, 'grantRole', [await burner.REQUEST_BURN_SHARES_ROLE(), nodeOperatorsRegistryAddress], { from: deployer })
+
+ await TotalGasCounter.incrementTotalGasUsedInStateFile()
+}
+
+module.exports = runOrWrapScript(deployNewContracts, module)
diff --git a/scripts/scratch/18-plug-curated-staking-module.js b/scripts/scratch/18-plug-curated-staking-module.js
new file mode 100644
index 000000000..d1cd011df
--- /dev/null
+++ b/scripts/scratch/18-plug-curated-staking-module.js
@@ -0,0 +1,61 @@
+const runOrWrapScript = require('../helpers/run-or-wrap-script')
+const { log, logSplitter, logWideSplitter, yl, gr } = require('../helpers/log')
+const { readNetworkState, assertRequiredNetworkState } = require('../helpers/persisted-network-state')
+
+const { APP_NAMES } = require('../constants')
+const { makeTx, TotalGasCounter } = require('../helpers/deploy')
+
+
+const REQUIRED_NET_STATE = [
+ "stakingRouter",
+ "app:node-operators-registry",
+ "deployer",
+ "app:aragon-agent",
+ `app:${APP_NAMES.LIDO}`,
+ `app:${APP_NAMES.ORACLE}`,
+ "app:aragon-voting",
+ "accountingOracle",
+ "burner",
+ "daoInitialSettings",
+ "eip712StETH",
+ "hashConsensusForAccountingOracle",
+ "hashConsensusForValidatorsExitBusOracle",
+ "lidoLocator",
+ "validatorsExitBusOracle",
+ "withdrawalQueueERC721",
+ "withdrawalVault",
+]
+
+const NOR_STAKING_MODULE_TARGET_SHARE_BP = 10000 // 100%
+const NOR_STAKING_MODULE_MODULE_FEE_BP = 500 // 5%
+const NOR_STAKING_MODULE_TREASURY_FEE_BP = 500 // 5%
+const STAKING_MODULE_MANAGE_ROLE = web3.utils.keccak256('STAKING_MODULE_MANAGE_ROLE')
+
+
+async function deployNewContracts({ web3, artifacts }) {
+ const netId = await web3.eth.net.getId()
+ logWideSplitter()
+ log(`Network ID:`, yl(netId))
+
+ let state = readNetworkState(network.name, netId)
+ assertRequiredNetworkState(state, REQUIRED_NET_STATE)
+
+ const deployer = state.deployer
+ const stakingRouter = await artifacts.require('StakingRouter').at(state.stakingRouter.proxy.address)
+ const nodeOperatorsRegistry = await artifacts.require('NodeOperatorsRegistry').at(state['app:node-operators-registry'].proxy.address)
+
+ await makeTx(stakingRouter, 'grantRole', [STAKING_MODULE_MANAGE_ROLE, deployer], { from: deployer })
+
+ await makeTx(stakingRouter, 'addStakingModule', [
+ state.nodeOperatorsRegistry.deployParameters.stakingModuleTypeId,
+ nodeOperatorsRegistry.address,
+ NOR_STAKING_MODULE_TARGET_SHARE_BP,
+ NOR_STAKING_MODULE_MODULE_FEE_BP,
+ NOR_STAKING_MODULE_TREASURY_FEE_BP,
+ ], { from: deployer })
+ await makeTx(stakingRouter, 'renounceRole', [STAKING_MODULE_MANAGE_ROLE, deployer], { from: deployer })
+
+ await TotalGasCounter.incrementTotalGasUsedInStateFile()
+}
+
+module.exports = runOrWrapScript(deployNewContracts, module)
diff --git a/scripts/scratch/19-transfer-roles.js b/scripts/scratch/19-transfer-roles.js
new file mode 100644
index 000000000..264e48148
--- /dev/null
+++ b/scripts/scratch/19-transfer-roles.js
@@ -0,0 +1,78 @@
+const runOrWrapScript = require('../helpers/run-or-wrap-script')
+const { log, logSplitter, logWideSplitter, yl, gr, OK } = require('../helpers/log')
+const { readNetworkState, assertRequiredNetworkState } = require('../helpers/persisted-network-state')
+const { makeTx, TotalGasCounter } = require('../helpers/deploy')
+
+const REQUIRED_NET_STATE = [
+ "app:aragon-agent",
+ "accountingOracle",
+ "burner",
+ "daoInitialSettings",
+ "hashConsensusForAccountingOracle",
+ "hashConsensusForValidatorsExitBusOracle",
+ "lidoLocator",
+ "stakingRouter",
+ "validatorsExitBusOracle",
+ "withdrawalQueueERC721",
+]
+
+const DEFAULT_ADMIN_ROLE = "0x00"
+
+
+async function transferOZAdmin(contractName, contractAddress, currentAdmin, newAdmin) {
+ console.log(`Transferring OZ admin of ${contractAddress} from ${currentAdmin} to ${newAdmin}:`)
+ const contract = await artifacts.require(contractName).at(contractAddress)
+ await makeTx(contract, 'grantRole', [DEFAULT_ADMIN_ROLE, newAdmin], { from: currentAdmin })
+ await makeTx(contract, 'renounceRole', [DEFAULT_ADMIN_ROLE, currentAdmin], { from: currentAdmin })
+ console.log()
+}
+
+async function changeOssifiableProxyAdmin(contractAddress, currentAdmin, newAdmin) {
+ console.log(`Transferring OssifiableProxy admin of ${contractAddress} from ${currentAdmin} to ${newAdmin}...`)
+ const contract = await artifacts.require('OssifiableProxy').at(contractAddress)
+ await makeTx(contract, 'proxy__changeAdmin', [newAdmin], { from: currentAdmin })
+ console.log()
+}
+
+async function changeDepositSecurityModuleAdmin(contractAddress, currentAdmin, newAdmin) {
+ console.log(`Changing DepositSecurityModule owner of ${contractAddress} from ${currentAdmin} to ${newAdmin}...`)
+ const depositSecurityModule = await artifacts.require('DepositSecurityModule').at(contractAddress)
+ await makeTx(depositSecurityModule, 'setOwner', [newAdmin], { from: currentAdmin } )
+ console.log()
+}
+
+async function deployNewContracts({ web3, artifacts }) {
+ const netId = await web3.eth.net.getId()
+ logWideSplitter()
+ log(`Network ID:`, yl(netId))
+
+ let state = readNetworkState(network.name, netId)
+ assertRequiredNetworkState(state, REQUIRED_NET_STATE)
+
+ const deployer = state.deployer
+ const agent = state["app:aragon-agent"].proxy.address
+
+ await transferOZAdmin('Burner', state.burner.address, deployer, agent)
+ await transferOZAdmin('HashConsensus', state.hashConsensusForAccountingOracle.address, deployer, agent)
+ await transferOZAdmin('HashConsensus', state.hashConsensusForValidatorsExitBusOracle.address, deployer, agent)
+ await transferOZAdmin('StakingRouter', state.stakingRouter.proxy.address, deployer, agent)
+ await transferOZAdmin('AccountingOracle', state.accountingOracle.proxy.address, deployer, agent)
+ await transferOZAdmin('ValidatorsExitBusOracle', state.validatorsExitBusOracle.proxy.address, deployer, agent)
+ await transferOZAdmin('WithdrawalQueueERC721', state.withdrawalQueueERC721.proxy.address, deployer, agent)
+ await transferOZAdmin('OracleDaemonConfig', state.oracleDaemonConfig.address, deployer, agent)
+ await transferOZAdmin('OracleReportSanityChecker', state.oracleReportSanityChecker.address, deployer, agent)
+
+ await changeOssifiableProxyAdmin(state.lidoLocator.proxy.address, deployer, agent)
+ await changeOssifiableProxyAdmin(state.stakingRouter.proxy.address, deployer, agent)
+ await changeOssifiableProxyAdmin(state.accountingOracle.proxy.address, deployer, agent)
+ await changeOssifiableProxyAdmin(state.validatorsExitBusOracle.proxy.address, deployer, agent)
+ await changeOssifiableProxyAdmin(state.withdrawalQueueERC721.proxy.address, deployer, agent)
+
+ if (state.depositSecurityModule.deployParameters.usePredefinedAddressInstead === null) {
+ await changeDepositSecurityModuleAdmin(state.depositSecurityModule.address, deployer, agent)
+ }
+
+ await TotalGasCounter.incrementTotalGasUsedInStateFile()
+}
+
+module.exports = runOrWrapScript(deployNewContracts, module)
diff --git a/scripts/scratch/checks/scratch-acceptance-test.js b/scripts/scratch/checks/scratch-acceptance-test.js
new file mode 100644
index 000000000..45339dd31
--- /dev/null
+++ b/scripts/scratch/checks/scratch-acceptance-test.js
@@ -0,0 +1,231 @@
+const { getEventArgument, ZERO_ADDRESS } = require('@aragon/contract-helpers-test')
+const runOrWrapScript = require('../../helpers/run-or-wrap-script')
+const { log, yl } = require('../../helpers/log')
+const { hexConcat, pad, ETH, e27, e18, toBN } = require('../../../test/helpers/utils')
+const { reportOracle } = require('../../../test/helpers/oracle')
+const { getBalance, advanceChainTime } = require('../../../test/helpers/blockchain')
+const { assertRequiredNetworkState, readStateFile } = require('../../helpers/persisted-network-state')
+const { assert } = require('../../../test/helpers/assert')
+
+
+const REQUIRED_NET_STATE = [
+ 'vestingParams',
+ 'daoInitialSettings',
+]
+
+const UNLIMITED = 1000000000
+const CURATED_MODULE_ID = 1
+const CALLDATA = '0x0'
+const MAX_DEPOSITS = 150
+const ADDRESS_1 = '0x0000000000000000000000000000000000000001'
+const ADDRESS_2 = '0x0000000000000000000000000000000000000002'
+
+const MANAGE_MEMBERS_AND_QUORUM_ROLE = web3.utils.keccak256('MANAGE_MEMBERS_AND_QUORUM_ROLE')
+
+if (!process.env.HARDHAT_FORKING_URL) {
+ console.error('Env variable HARDHAT_FORKING_URL must be set to run fork acceptance tests')
+ process.exit(1);
+}
+if (!process.env.NETWORK_STATE_FILE) {
+ console.error('Env variable NETWORK_STATE_FILE must be set to run fork acceptance tests')
+ process.exit(1);
+}
+const NETWORK_STATE_FILE = process.env.NETWORK_STATE_FILE
+
+
+
+async function loadDeployedProtocol(state) {
+ return {
+ stakingRouter: await artifacts.require('StakingRouter').at(state.stakingRouter.proxy.address),
+ lido: await artifacts.require('Lido').at(state['app:lido'].proxy.address),
+ voting: await artifacts.require('Voting').at(state['app:aragon-voting'].proxy.address),
+ agent: await artifacts.require('Agent').at(state['app:aragon-agent'].proxy.address),
+ nodeOperatorsRegistry: await artifacts.require('NodeOperatorsRegistry').at(state['app:node-operators-registry'].proxy.address),
+ depositSecurityModuleAddress: state.depositSecurityModule.address, // Deploying DepositSecurityModule might be omitted and e.g. EOA is used
+ accountingOracle: await artifacts.require('AccountingOracle').at(state.accountingOracle.proxy.address),
+ hashConsensusForAO: await artifacts.require('HashConsensus').at(state.hashConsensusForAccountingOracle.address),
+ elRewardsVault: await artifacts.require('LidoExecutionLayerRewardsVault').at(state.executionLayerRewardsVault.address),
+ withdrawalQueue: await artifacts.require('WithdrawalQueueERC721').at(state.withdrawalQueueERC721.proxy.address),
+ ldo: await artifacts.require('MiniMeToken').at(state.ldo.address),
+ }
+}
+
+
+async function checkLDOCanBeTransferred(ldo, state) {
+ const ldoHolder = Object.keys(state.vestingParams.holders)[0]
+ await ethers.provider.send('hardhat_impersonateAccount', [ldoHolder])
+
+ await ldo.transfer(ADDRESS_1, e18(1), { from: ldoHolder })
+ assert.equals(await ldo.balanceOf(ADDRESS_1), e18(1))
+
+ log.success("Transferred LDO")
+}
+
+
+async function prepareProtocolForSubmitDepositReportWithdrawalFlow(protocol, state, oracleMember1, oracleMember2) {
+ const {
+ lido,
+ voting,
+ agent,
+ nodeOperatorsRegistry,
+ depositSecurityModuleAddress,
+ hashConsensusForAO,
+ withdrawalQueue,
+ } = protocol
+
+ await ethers.provider.send('hardhat_impersonateAccount', [voting.address])
+ await ethers.provider.send('hardhat_impersonateAccount', [depositSecurityModuleAddress])
+ await ethers.provider.send('hardhat_impersonateAccount', [agent.address])
+
+ await lido.resume({ from: voting.address })
+
+ await withdrawalQueue.grantRole(await withdrawalQueue.RESUME_ROLE(), agent.address, { from: agent.address })
+ await withdrawalQueue.resume({ from: agent.address })
+ await withdrawalQueue.renounceRole(await withdrawalQueue.RESUME_ROLE(), agent.address, { from: agent.address })
+
+ await nodeOperatorsRegistry.addNodeOperator('1', ADDRESS_1, { from: agent.address })
+ await nodeOperatorsRegistry.addNodeOperator('2', ADDRESS_2, { from: agent.address })
+
+ await nodeOperatorsRegistry.addSigningKeys(0, 1, pad('0x010203', 48), pad('0x01', 96), { from: voting.address })
+ await nodeOperatorsRegistry.addSigningKeys(
+ 0,
+ 3,
+ hexConcat(pad('0x010204', 48), pad('0x010205', 48), pad('0x010206', 48)),
+ hexConcat(pad('0x01', 96), pad('0x01', 96), pad('0x01', 96)),
+ { from: voting.address }
+ )
+
+ await nodeOperatorsRegistry.setNodeOperatorStakingLimit(0, UNLIMITED, { from: voting.address })
+ await nodeOperatorsRegistry.setNodeOperatorStakingLimit(1, UNLIMITED, { from: voting.address })
+
+ const quorum = 2
+ await hashConsensusForAO.grantRole(MANAGE_MEMBERS_AND_QUORUM_ROLE, agent.address, { from: agent.address })
+ await hashConsensusForAO.addMember(oracleMember1.address, quorum, { from: agent.address })
+ await hashConsensusForAO.addMember(oracleMember2.address, quorum, { from: agent.address })
+ await hashConsensusForAO.renounceRole(MANAGE_MEMBERS_AND_QUORUM_ROLE, agent.address, { from: agent.address })
+
+ log.success('Protocol prepared for submit-deposit-report-withdraw flow')
+}
+
+async function checkSubmitDepositReportWithdrawal(protocol, state, user1, user2) {
+ const {
+ lido,
+ agent,
+ depositSecurityModuleAddress,
+ accountingOracle,
+ hashConsensusForAO,
+ elRewardsVault,
+ withdrawalQueue,
+ } = protocol
+
+
+ const initialLidoBalance = await getBalance(lido.address)
+ const chainSpec = state.chainSpec
+
+ await user1.sendTransaction({ to: lido.address, value: ETH(34) })
+ await user2.sendTransaction({ to: elRewardsVault.address, value: ETH(1) })
+ log.success('Users submitted ether')
+
+ assert.equals(await lido.balanceOf(user1.address), ETH(34))
+ assert.equals(await lido.getTotalPooledEther(), initialLidoBalance + BigInt(ETH(34)))
+ assert.equals(await lido.getBufferedEther(), initialLidoBalance + BigInt(ETH(34)))
+
+ await lido.deposit(MAX_DEPOSITS, CURATED_MODULE_ID, CALLDATA, { from: depositSecurityModuleAddress })
+ log.success('Ether deposited')
+
+
+ assert.equals((await lido.getBeaconStat()).depositedValidators, 1)
+
+ const latestBlockTimestamp = (await ethers.provider.getBlock('latest')).timestamp
+ const initialEpoch = Math.floor((latestBlockTimestamp - chainSpec.genesisTime)
+ / (chainSpec.slotsPerEpoch * chainSpec.secondsPerSlot))
+
+ await hashConsensusForAO.updateInitialEpoch(initialEpoch, { from: agent.address })
+
+
+ const elRewardsVaultBalance = await web3.eth.getBalance(elRewardsVault.address)
+
+ const withdrawalAmount = ETH(1)
+
+ await lido.approve(withdrawalQueue.address, withdrawalAmount, { from: user1.address })
+ const receipt = await withdrawalQueue.requestWithdrawals([withdrawalAmount], user1.address, { from: user1.address })
+ const requestId = getEventArgument(receipt, 'WithdrawalRequested', 'requestId')
+
+ log.success('Withdrawal request made')
+
+ const epochsPerFrame = +(await hashConsensusForAO.getFrameConfig()).epochsPerFrame
+ const initialEpochTimestamp = chainSpec.genesisTime + initialEpoch * chainSpec.slotsPerEpoch * chainSpec.secondsPerSlot
+
+ // skip two reports to be sure about REQUEST_TIMESTAMP_MARGIN
+ const nextReportEpochTimestamp = initialEpochTimestamp + 2 * epochsPerFrame * chainSpec.slotsPerEpoch * chainSpec.secondsPerSlot
+
+ const timeToWaitTillReportWindow = nextReportEpochTimestamp - latestBlockTimestamp + chainSpec.secondsPerSlot
+
+ await advanceChainTime(timeToWaitTillReportWindow)
+
+ let stat = await lido.getBeaconStat()
+ const clBalance = toBN(stat.depositedValidators).mul(toBN(e18(32)))
+
+
+ const { refSlot } = await hashConsensusForAO.getCurrentFrame()
+ const reportTimestamp = +chainSpec.genesisTime + (+refSlot) * (+chainSpec.secondsPerSlot)
+ const timeElapsed = +(nextReportEpochTimestamp - initialEpochTimestamp)
+
+ const withdrawalFinalizationBatches = [1]
+
+ // Performing dry-run to estimate simulated share rate
+ const [postTotalPooledEther, postTotalShares, withdrawals, elRewards] = await lido.handleOracleReport.call(
+ reportTimestamp,
+ timeElapsed,
+ stat.depositedValidators,
+ clBalance,
+ 0 /* withdrawals vault balance */,
+ elRewardsVaultBalance,
+ 0 /* shares requested to burn */,
+ [] /* withdrawal finalization batches */,
+ 0 /* simulated share rate */,
+ { from: accountingOracle.address }
+ )
+
+ log.success('Oracle report simulated')
+
+ const simulatedShareRate = postTotalPooledEther.mul(toBN(e27(1))).div(postTotalShares)
+
+ await reportOracle(hashConsensusForAO, accountingOracle, {
+ refSlot,
+ numValidators: stat.depositedValidators,
+ clBalance,
+ elRewardsVaultBalance,
+ withdrawalFinalizationBatches,
+ simulatedShareRate,
+ })
+
+ log.success('Oracle report submitted')
+
+ await withdrawalQueue.claimWithdrawalsTo([requestId], [requestId], user1.address, { from: user1.address })
+
+ log.success('Withdrawal claimed successfully')
+}
+
+async function checkMainProtocolFlows({ web3 }) {
+ const netId = await web3.eth.net.getId()
+
+ log.splitter()
+ log(`Network ID: ${yl(netId)}`)
+
+ const state = readStateFile(NETWORK_STATE_FILE)
+ assertRequiredNetworkState(state, REQUIRED_NET_STATE)
+
+ log.splitter()
+
+ const protocol = await loadDeployedProtocol(state)
+ const [user1, user2, oracleMember1, oracleMember2] = await ethers.getSigners()
+
+ await checkLDOCanBeTransferred(protocol.ldo, state)
+
+ await prepareProtocolForSubmitDepositReportWithdrawalFlow(protocol, state, oracleMember1, oracleMember2)
+ await checkSubmitDepositReportWithdrawal(protocol, state, user1, user2)
+}
+
+
+module.exports = runOrWrapScript(checkMainProtocolFlows, module)
diff --git a/scripts/scratch/checks/scratch-deploy-permissions.json b/scripts/scratch/checks/scratch-deploy-permissions.json
new file mode 100644
index 000000000..f179011af
--- /dev/null
+++ b/scripts/scratch/checks/scratch-deploy-permissions.json
@@ -0,0 +1,132 @@
+{
+ "lidoLocator": {
+ "OssifiableProxy": {
+ "admin": "app:aragon-agent"
+ }
+ },
+ "burner": {
+ "AccessControlEnumerable": {
+ "roles": {
+ "DEFAULT_ADMIN_ROLE": ["app:aragon-agent"],
+ "REQUEST_BURN_MY_STETH_ROLE": [],
+ "REQUEST_BURN_SHARES_ROLE": ["app:lido", "app:node-operators-registry"]
+ }
+ }
+ },
+ "stakingRouter": {
+ "AccessControlEnumerable": {
+ "roles": {
+ "DEFAULT_ADMIN_ROLE": ["app:aragon-agent"],
+ "MANAGE_WITHDRAWAL_CREDENTIALS_ROLE": [],
+ "STAKING_MODULE_PAUSE_ROLE": ["depositSecurityModule"],
+ "STAKING_MODULE_RESUME_ROLE": ["depositSecurityModule"],
+ "STAKING_MODULE_MANAGE_ROLE": [],
+ "REPORT_EXITED_VALIDATORS_ROLE": ["accountingOracle"],
+ "UNSAFE_SET_EXITED_VALIDATORS_ROLE": [],
+ "REPORT_REWARDS_MINTED_ROLE": ["app:lido"]
+ }
+ },
+ "OssifiableProxy": {
+ "admin": "app:aragon-agent"
+ }
+ },
+ "withdrawalQueueERC721": {
+ "AccessControlEnumerable": {
+ "roles": {
+ "DEFAULT_ADMIN_ROLE": ["app:aragon-agent"],
+ "PAUSE_ROLE": ["gateSeal"],
+ "RESUME_ROLE": [],
+ "FINALIZE_ROLE": ["app:lido"],
+ "ORACLE_ROLE": ["accountingOracle"],
+ "MANAGE_TOKEN_URI_ROLE": []
+ }
+ },
+ "OssifiableProxy": {
+ "admin": "app:aragon-agent"
+ }
+ },
+ "accountingOracle": {
+ "AccessControlEnumerable": {
+ "roles": {
+ "DEFAULT_ADMIN_ROLE": ["app:aragon-agent"],
+ "SUBMIT_DATA_ROLE": [],
+ "MANAGE_CONSENSUS_CONTRACT_ROLE": [],
+ "MANAGE_CONSENSUS_VERSION_ROLE": []
+ }
+ },
+ "OssifiableProxy": {
+ "admin": "app:aragon-agent"
+ }
+ },
+ "validatorsExitBusOracle": {
+ "AccessControlEnumerable": {
+ "roles": {
+ "DEFAULT_ADMIN_ROLE": ["app:aragon-agent"],
+ "SUBMIT_DATA_ROLE": [],
+ "PAUSE_ROLE": ["gateSeal"],
+ "RESUME_ROLE": [],
+ "MANAGE_CONSENSUS_CONTRACT_ROLE": [],
+ "MANAGE_CONSENSUS_VERSION_ROLE": []
+ }
+ },
+ "OssifiableProxy": {
+ "admin": "app:aragon-agent"
+ }
+ },
+ "hashConsensusForAccountingOracle": {
+ "AccessControlEnumerable": {
+ "roles": {
+ "DEFAULT_ADMIN_ROLE": ["app:aragon-agent"],
+ "MANAGE_MEMBERS_AND_QUORUM_ROLE": [],
+ "DISABLE_CONSENSUS_ROLE": [],
+ "MANAGE_FRAME_CONFIG_ROLE": [],
+ "MANAGE_FAST_LANE_CONFIG_ROLE": [],
+ "MANAGE_REPORT_PROCESSOR_ROLE": []
+ }
+ }
+ },
+ "hashConsensusForValidatorsExitBusOracle": {
+ "AccessControlEnumerable": {
+ "roles": {
+ "DEFAULT_ADMIN_ROLE": ["app:aragon-agent"],
+ "MANAGE_MEMBERS_AND_QUORUM_ROLE": [],
+ "DISABLE_CONSENSUS_ROLE": [],
+ "MANAGE_FRAME_CONFIG_ROLE": [],
+ "MANAGE_FAST_LANE_CONFIG_ROLE": [],
+ "MANAGE_REPORT_PROCESSOR_ROLE": []
+ }
+ }
+ },
+ "oracleReportSanityChecker": {
+ "AccessControlEnumerable": {
+ "roles": {
+ "DEFAULT_ADMIN_ROLE": ["app:aragon-agent"],
+ "ALL_LIMITS_MANAGER_ROLE": [],
+ "CHURN_VALIDATORS_PER_DAY_LIMIT_MANAGER_ROLE": [],
+ "ONE_OFF_CL_BALANCE_DECREASE_LIMIT_MANAGER_ROLE": [],
+ "ANNUAL_BALANCE_INCREASE_LIMIT_MANAGER_ROLE": [],
+ "SHARE_RATE_DEVIATION_LIMIT_MANAGER_ROLE": [],
+ "MAX_VALIDATOR_EXIT_REQUESTS_PER_REPORT_ROLE": [],
+ "MAX_ACCOUNTING_EXTRA_DATA_LIST_ITEMS_COUNT_ROLE": [],
+ "MAX_NODE_OPERATORS_PER_EXTRA_DATA_ITEM_COUNT_ROLE": [],
+ "REQUEST_TIMESTAMP_MARGIN_MANAGER_ROLE": [],
+ "MAX_POSITIVE_TOKEN_REBASE_MANAGER_ROLE": []
+ }
+ }
+ },
+ "depositSecurityModule": {
+ "DepositSecurityModule": {
+ "specificViews": {
+ "getOwner": "app:aragon-agent"
+ }
+ }
+ },
+ "oracleDaemonConfig": {
+ "AccessControlEnumerable": {
+ "roles": {
+ "DEFAULT_ADMIN_ROLE": ["app:aragon-agent"],
+ "CONFIG_MANAGER_ROLE": []
+ }
+ }
+ }
+}
diff --git a/scripts/scratch/dao-deploy.sh b/scripts/scratch/dao-deploy.sh
new file mode 100755
index 000000000..4b3b5bf44
--- /dev/null
+++ b/scripts/scratch/dao-deploy.sh
@@ -0,0 +1,94 @@
+#!/bin/bash
+set -e +u
+set -o pipefail
+
+ARAGON_APPS_REPO_REF=import-shared-minime
+
+if [[ -z "${DEPLOYER}" ]]; then
+ echo "Env variable DEPLOYER must be set"
+ exit 1
+fi
+echo "DEPLOYER is $DEPLOYER"
+
+if [[ -z "${NETWORK}" ]]; then
+ echo "Env variable NETWORK must be set"
+ exit 1
+fi
+echo "NETWORK is $NETWORK"
+
+function msg() {
+ MSG=$1
+ if [ ! -z "$MSG" ]; then
+ echo ">>> ============================="
+ echo ">>> $MSG"
+ echo ">>> ============================="
+ fi
+}
+
+
+# yarn install --immutable
+yarn compile
+
+rm -f ${NETWORK_STATE_FILE}
+cp ${NETWORK_STATE_DEFAULTS_FILE} ${NETWORK_STATE_FILE}
+
+
+# Fill in deployer, chainId, etc from env to the deploy artifact
+yarn hardhat --network $NETWORK run ./scripts/scratch/00-populate-deploy-artifact-from-env.js --no-compile
+
+# It does not deploy DepositContract if it is specified in deployed-${NETWORK}-defaults.json
+yarn hardhat --network $NETWORK run ./scripts/scratch/01-deploy-deposit-contract.js --no-compile
+msg "Deposit contract deployed or is specified."
+
+yarn hardhat --network $NETWORK run --no-compile ./scripts/scratch/02-deploy-aragon-env.js
+msg "Aragon ENV deployed."
+
+yarn hardhat run --no-compile ./scripts/scratch/03-deploy-aragon-std-apps.js --network $NETWORK
+msg "Aragon STD apps deployed."
+
+yarn hardhat --network $NETWORK run ./scripts/scratch/04-deploy-lido-template-and-bases.js --no-compile
+yarn hardhat --network $NETWORK run ./scripts/scratch/05-obtain-deployed-instances.js --no-compile
+msg "Apps instances deployed"
+
+yarn hardhat --network $NETWORK run ./scripts/scratch/06-register-ens-domain.js --no-compile
+msg "ENS registered"
+
+yarn hardhat --network $NETWORK run ./scripts/scratch/07-deploy-apm.js --no-compile
+yarn hardhat --network $NETWORK run ./scripts/scratch/08-obtain-deployed-apm.js --no-compile
+msg "APM deployed"
+
+yarn hardhat --network $NETWORK run ./scripts/scratch/09-create-app-repos.js --no-compile
+msg "App repos created"
+
+yarn hardhat --network $NETWORK run ./scripts/scratch/10-deploy-dao.js --no-compile
+
+yarn hardhat --network $NETWORK run ./scripts/scratch/11-obtain-deployed-dao.js --no-compile
+msg "DAO deploy started"
+
+# Do it at the end, because might need the contracts initialized
+yarn hardhat --network $NETWORK run ./scripts/scratch/12-issue-tokens.js --no-compile
+msg "Tokens issued"
+
+# Deploy the contracts before finalizing DAO, because the template might set permissions on some of them
+yarn hardhat --network $NETWORK run ./scripts/scratch/13-deploy-non-aragon-contracts.js --no-compile
+msg "Non-aragon contracts deployed"
+
+yarn hardhat --network $NETWORK run ./scripts/scratch/14-gate-seal.js --no-compile
+
+yarn hardhat --network $NETWORK run ./scripts/scratch/15-finalize-dao.js --no-compile
+msg "DAO deploy finalized"
+
+yarn hardhat --network $NETWORK run ./scripts/scratch/16-initialize-non-aragon-contracts.js --no-compile
+msg "Non-aragon contracts initialized"
+
+yarn hardhat --network $NETWORK run ./scripts/scratch/17-grant-roles.js --no-compile
+msg "Roles granted"
+
+yarn hardhat --network $NETWORK run ./scripts/scratch/18-plug-curated-staking-module.js --no-compile
+msg "Plugged NodeOperatorsRegistry as Curated staking module"
+
+yarn hardhat --network $NETWORK run ./scripts/scratch/19-transfer-roles.js --no-compile
+msg "Role admin permissions transferred to Agent"
+
+yarn hardhat --network $NETWORK run ./scripts/scratch/20-check-dao.js --no-compile
+msg "The deployed protocol state checked"
diff --git a/scripts/scratch/dao-holesky-deploy.sh b/scripts/scratch/dao-holesky-deploy.sh
new file mode 100755
index 000000000..1927d62cf
--- /dev/null
+++ b/scripts/scratch/dao-holesky-deploy.sh
@@ -0,0 +1,31 @@
+#!/bin/bash
+set -e +u
+set -o pipefail
+
+if [[ -z "$DEPLOYER" ]]; then
+ echo "Must set DEPLOYER env variable" 1>&2
+ exit 1
+fi
+if [[ -z "$RPC_URL" ]]; then
+ echo "Must set RPC_URL env variable" 1>&2
+ exit 1
+fi
+if [[ -z "$GATE_SEAL_FACTORY" ]]; then
+ echo "Must set GATE_SEAL_FACTORY env variable" 1>&2
+ exit 1
+fi
+
+export NETWORK=holesky
+export NETWORK_STATE_FILE="deployed-${NETWORK}.json"
+export NETWORK_STATE_DEFAULTS_FILE="deployed-testnet-defaults.json"
+
+# Holesky params: https://github.com/eth-clients/holesky/blob/main/README.md
+export GENESIS_TIME=1695902400
+export DEPOSIT_CONTRACT=0x4242424242424242424242424242424242424242
+
+# export WITHDRAWAL_QUEUE_BASE_URI="<< SET IF REQUIED >>"
+
+export GAS_PRIORITY_FEE="${GAS_PRIORITY_FEE:=1}"
+export GAS_MAX_FEE="${GAS_MAX_FEE:=100}"
+
+bash scripts/scratch/dao-deploy.sh
diff --git a/scripts/scratch/dao-local-deploy.sh b/scripts/scratch/dao-local-deploy.sh
new file mode 100755
index 000000000..7adc6fff0
--- /dev/null
+++ b/scripts/scratch/dao-local-deploy.sh
@@ -0,0 +1,27 @@
+#!/bin/bash
+set -e +u
+set -o pipefail
+
+#
+export NETWORK=local
+export RPC_URL=${RPC_URL:="http://127.0.0.1:8555"} # if defined use the value set to default otherwise
+
+# If GateSeal factory is zero, deploy no GateSeal instance. Otherwise use the factory to deploy an instance
+export GATE_SEAL_FACTORY=0x0000000000000000000000000000000000000000
+export GENESIS_TIME=1639659600 # just some time
+# export WITHDRAWAL_QUEUE_BASE_URI="<< SET IF REQUIED >>"
+# export DSM_PREDEFINED_ADDRESS="<< SET IF REQUIED >>"
+#
+export DEPLOYER=0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
+export GAS_PRIORITY_FEE=1
+export GAS_MAX_FEE=100
+#
+export NETWORK_STATE_FILE="deployed-${NETWORK}.json"
+export NETWORK_STATE_DEFAULTS_FILE="deployed-testnet-defaults.json"
+
+bash scripts/scratch/dao-deploy.sh
+
+# # Need this to get sure the last transactions are mined
+yarn hardhat --network $NETWORK run ./scripts/scratch/send-hardhat-mine.js --no-compile
+
+NETWORK_STATE_FILE=deployed-local.json HARDHAT_FORKING_URL="${RPC_URL}" yarn hardhat run --no-compile ./scripts/scratch/checks/scratch-acceptance-test.js --network hardhat
\ No newline at end of file
diff --git a/scripts/scratch/dao-sepolia-deploy.sh b/scripts/scratch/dao-sepolia-deploy.sh
new file mode 100755
index 000000000..7bff37abe
--- /dev/null
+++ b/scripts/scratch/dao-sepolia-deploy.sh
@@ -0,0 +1,35 @@
+#!/bin/bash
+set -e +u
+set -o pipefail
+
+if [[ -z "$DEPLOYER" ]]; then
+ echo "Must set DEPLOYER env variable" 1>&2
+ exit 1
+fi
+if [[ -z "$RPC_URL" ]]; then
+ echo "Must set RPC_URL env variable" 1>&2
+ exit 1
+fi
+
+export GATE_SEAL_FACTORY=0x0000000000000000000000000000000000000000
+export NETWORK=sepolia
+export NETWORK_STATE_FILE="deployed-${NETWORK}.json"
+export NETWORK_STATE_DEFAULTS_FILE="deployed-testnet-defaults.json"
+
+# Sepolia params: https://github.com/eth-clients/sepolia/blob/main/README.md
+export GENESIS_TIME=1655733600
+
+# EOA
+export DSM_PREDEFINED_ADDRESS="0x6885E36BFcb68CB383DfE90023a462C03BCB2AE5"
+
+export GAS_PRIORITY_FEE="${GAS_PRIORITY_FEE:=1}"
+export GAS_MAX_FEE="${GAS_MAX_FEE:=100}"
+
+# Deposit adapter depoyment / upgrading
+# yarn hardhat --network $NETWORK run ./scripts/deploy-sepolia-deposit-contract-adapter.js --no-compile
+
+# Deposit contract custom LIDO adapter
+# deployed from scripts/deploy-sepolia-deposit-contract-adapter.js
+export DEPOSIT_CONTRACT="0x80b5DC88C98E528bF9cb4B7F0f076aC41da24651"
+
+bash scripts/scratch/dao-deploy.sh
diff --git a/scripts/scratch/send-hardhat-mine.js b/scripts/scratch/send-hardhat-mine.js
new file mode 100644
index 000000000..1f07f7950
--- /dev/null
+++ b/scripts/scratch/send-hardhat-mine.js
@@ -0,0 +1,10 @@
+const runOrWrapScript = require('../helpers/run-or-wrap-script')
+const { log } = require('../helpers/log')
+
+async function main() {
+ // 0x01 is too little, 0x80 works, although less might be enough
+ await ethers.provider.send('hardhat_mine', ["0x80"])
+ log.success(`Send "hardhat_mine"`)
+}
+
+module.exports = runOrWrapScript(main, module)
diff --git a/scripts/scratch/verify-contracts-code.sh b/scripts/scratch/verify-contracts-code.sh
new file mode 100644
index 000000000..9f260adaf
--- /dev/null
+++ b/scripts/scratch/verify-contracts-code.sh
@@ -0,0 +1,89 @@
+#!/bin/bash
+set -e
+SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
+
+
+if [[ -z "$NETWORK" ]]; then
+ echo "Must set NETWORK env variable" 1>&2
+ exit 1
+fi
+
+NETWORK_STATE_FILE="deployed-${NETWORK}.json"
+if [ ! -f $NETWORK_STATE_FILE ]; then
+ echo "Cannot find network state file ${NETWORK_STATE_FILE}"
+ exit 1
+fi
+echo "Using network state file ${NETWORK_STATE_FILE}"
+
+function jsonGet {
+ node -e "const fs = require('fs'); const obj = JSON.parse(fs.readFileSync('${NETWORK_STATE_FILE}', 'utf8')); const path='$1'; let res = path.split('.').reduce(function(o, k) {return o && o[k] }, obj); console.log(res)"
+}
+
+function verify {
+ contractPath="$(jsonGet ${1}.contract)"
+ contractName="${contractPath##*/}"
+ contractName="${contractName%.*}"
+ argsJson=$(jsonGet ${1}.constructorArgs)
+ echo "module.exports = $argsJson" > contract-args.js
+ yarn hardhat --network $NETWORK verify --no-compile --contract "$contractPath:$contractName" --constructor-args contract-args.js $(jsonGet ${1}.address)
+}
+
+# NB: Although most of the contracts listed below would be verified by running
+# this bash script as it is, some might require some manual tweaking.
+# Sometimes first attempt to verify fails without observable reason.
+# Part of the contract require a workaround see scratch-deploy.md section
+# "Issues with verification of part of the contracts deployed from factories".
+
+verify dummyEmptyContract
+verify burner
+verify hashConsensusForAccountingOracle
+verify hashConsensusForValidatorsExitBusOracle
+verify accountingOracle.implementation
+verify accountingOracle.proxy
+verify validatorsExitBusOracle.implementation
+verify validatorsExitBusOracle.proxy
+verify stakingRouter.implementation
+verify stakingRouter.proxy
+verify withdrawalQueueERC721.proxy
+verify wstETH
+verify executionLayerRewardsVault
+verify eip712StETH
+verify lidoTemplate
+verify withdrawalVault.proxy
+verify withdrawalVault.implementation
+verify lidoLocator.proxy
+verify lidoLocator.implementation
+verify app:lido.implementation
+verify app:oracle.implementation
+verify app:node-operators-registry.implementation
+verify app:aragon-voting.implementation
+verify app:aragon-token-manager.implementation
+verify app:aragon-finance.implementation
+verify app:aragon-agent.implementation
+verify oracleDaemonConfig
+verify oracleReportSanityChecker
+verify fakeAppProxyPinned
+verify app:lido.proxy
+verify depositSecurityModule
+verify withdrawalQueueERC721.implementation
+verify aragon-kernel.implementation
+verify aragon-acl.implementation
+verify aragon-kernel.proxy
+verify ldo
+verify callsScript
+verify aragon-evm-script-registry.proxy
+verify aragon-apm-registry.implementation
+verify aragon-apm-registry.factory
+verify aragon-app-repo-lido.implementation
+verify aragon-app-repo-node-operators-registry.implementation
+# NB: App Repos of lido, oracle, node-operators-registry, finance, agent, token-manager, voting
+# share same implementation of Repo contract
+verify aragon-evm-script-registry.proxy
+verify aragon-evm-script-registry.implementation
+verify app:simple-dvt.proxy
+verify app:aragon-token-manager.proxy
+verify app:oracle.proxy
+verify app:node-operators-registry.proxy
+verify app:aragon-voting.proxy
+verify app:aragon-finance.proxy
+verify app:aragon-agent.proxy
diff --git a/scripts/simpledvt/01-deploy-app-proxy.js b/scripts/simpledvt/01-deploy-app-proxy.js
new file mode 100644
index 000000000..21edadf75
--- /dev/null
+++ b/scripts/simpledvt/01-deploy-app-proxy.js
@@ -0,0 +1,93 @@
+const { network } = require('hardhat')
+const runOrWrapScript = require('../helpers/run-or-wrap-script')
+const { log, yl } = require('../helpers/log')
+const { getDeployer, readStateAppAddress, _checkEq, _pause } = require('./helpers')
+const {
+ readNetworkState,
+ assertRequiredNetworkState,
+ persistNetworkState,
+} = require('../helpers/persisted-network-state')
+
+const { hash: namehash } = require('eth-ens-namehash')
+const { ZERO_ADDRESS } = require('../../test/helpers/utils')
+
+const APP_TRG = process.env.APP_TRG || 'simple-dvt'
+const DEPLOYER = process.env.DEPLOYER || ''
+
+const REQUIRED_NET_STATE = ['lidoApm', 'lidoApmEnsName']
+
+async function deployEmptyProxy({ web3, artifacts, trgAppName = APP_TRG }) {
+ const netId = await web3.eth.net.getId()
+ const deployer = await getDeployer(web3, DEPLOYER)
+
+ log.splitter()
+ log(`Network ID: ${yl(netId)}`)
+ log(`Deployer: ${yl(deployer)}`)
+
+ const state = readNetworkState(network.name, netId)
+ assertRequiredNetworkState(state, REQUIRED_NET_STATE)
+
+ const trgAppFullName = `${trgAppName}.${state.lidoApmEnsName}`
+ const trgAppId = namehash(trgAppFullName)
+
+ const kernelAddress = state.daoAddress || readStateAppAddress(state, `aragon-kernel`)
+ if (!kernelAddress) {
+ throw new Error(`No Aragon kernel (DAO address) found!`)
+ }
+
+ log.splitter()
+ log(`DAO:`, yl(kernelAddress))
+ log(`Target App:`, yl(trgAppName))
+ log(`Target App ENS:`, yl(trgAppFullName))
+ log(`Target App ID:`, yl(trgAppId))
+ log.splitter()
+
+ let trgProxyAddress
+
+ if (state[`app:${trgAppName}`]) {
+ trgProxyAddress = readStateAppAddress(state, `app:${trgAppName}`)
+ }
+
+ if (!trgProxyAddress || (await web3.eth.getCode(trgProxyAddress)) === '0x') {
+ await _pause('Ready for TX')
+ log.splitter()
+
+ const kernel = await artifacts.require('Kernel').at(kernelAddress)
+ const tx = await log.tx(
+ `Deploying proxy for ${trgAppName}`,
+ kernel.newAppProxy(kernelAddress, trgAppId, { from: deployer })
+ )
+ // Find the deployed proxy address in the tx logs.
+ const e = tx.logs.find((l) => l.event === 'NewAppProxy')
+ trgProxyAddress = e.args.proxy
+
+ // upd deployed state
+ persistNetworkState(network.name, netId, state, {
+ [`app:${trgAppName}`]: {
+ aragonApp: {
+ name: trgAppName,
+ fullName: trgAppFullName,
+ id: trgAppId,
+ },
+ proxy: {
+ address: trgProxyAddress,
+ contract: '@aragon/os/contracts/apps/AppProxyUpgradeable.sol',
+ constructorArgs: [kernelAddress, trgAppId, '0x'],
+ },
+ },
+ })
+ }
+
+ log(`Target app proxy deployed at`, yl(trgProxyAddress))
+
+ log.splitter()
+ log('Checking deployed proxy...')
+
+ const proxy = await artifacts.require('AppProxyUpgradeable').at(trgProxyAddress)
+
+ _checkEq(await proxy.kernel(), kernelAddress, 'App proxy kernel address matches Lido DAO')
+ _checkEq(await proxy.appId(), trgAppId, 'App proxy AppId matches SimpleDVT')
+ _checkEq(await proxy.implementation(), ZERO_ADDRESS, 'App proxy has ZERO_ADDRESS implementations')
+}
+
+module.exports = runOrWrapScript(deployEmptyProxy, module)
diff --git a/scripts/simpledvt/02-clone-nor.js b/scripts/simpledvt/02-clone-nor.js
new file mode 100644
index 000000000..6a617a923
--- /dev/null
+++ b/scripts/simpledvt/02-clone-nor.js
@@ -0,0 +1,553 @@
+const { network, ethers } = require('hardhat')
+const { Contract } = require('ethers')
+const { encodeCallScript } = require('@aragon/contract-helpers-test/src/aragon-os')
+const { getEventArgument } = require('@aragon/contract-helpers-test')
+const { EVMScriptDecoder, abiProviders } = require('evm-script-decoder')
+const runOrWrapScript = require('../helpers/run-or-wrap-script')
+const { log, yl, gr, cy } = require('../helpers/log')
+// const { saveCallTxData } = require('../helpers/tx-data')
+const { resolveLatestVersion } = require('../components/apm')
+const {
+ readNetworkState,
+ assertRequiredNetworkState,
+ persistNetworkState,
+} = require('../helpers/persisted-network-state')
+const { resolveEnsAddress } = require('../components/ens')
+const { hash: namehash } = require('eth-ens-namehash')
+const { APP_NAMES, APP_ARTIFACTS } = require('../constants')
+const {
+ getDeployer,
+ readStateAppAddress,
+ getSignature,
+ KERNEL_APP_BASES_NAMESPACE,
+ MANAGE_SIGNING_KEYS,
+ MANAGE_NODE_OPERATOR_ROLE,
+ SET_NODE_OPERATOR_LIMIT_ROLE,
+ STAKING_ROUTER_ROLE,
+ STAKING_MODULE_MANAGE_ROLE,
+ REQUEST_BURN_SHARES_ROLE,
+ SIMPLE_DVT_IPFS_CID,
+ easyTrackABI,
+ easyTrackFactoryABI,
+ _pause,
+ _checkLog,
+ _checkEqLog,
+} = require('./helpers')
+const { ETH, toBN } = require('../../test/helpers/utils')
+
+const APP_TRG = process.env.APP_TRG || APP_NAMES.SIMPLE_DVT
+const APP_IPFS_CID = process.env.APP_IPFS_CID || SIMPLE_DVT_IPFS_CID
+const DEPLOYER = process.env.DEPLOYER || ''
+
+const SIMULATE = !!process.env.SIMULATE
+const VOTE_ID = process.env.VOTE_ID || ''
+
+const REQUIRED_NET_STATE = [
+ 'ensAddress',
+ 'lidoApm',
+ 'lidoApmEnsName',
+ 'lidoLocator',
+ `app:${APP_NAMES.ARAGON_VOTING}`,
+ `app:${APP_NAMES.ARAGON_TOKEN_MANAGER}`,
+]
+
+async function deploySimpleDVT({ web3, artifacts, trgAppName = APP_TRG, ipfsCid = APP_IPFS_CID }) {
+ const netId = await web3.eth.net.getId()
+ const deployer = await getDeployer(web3, DEPLOYER)
+
+ log.splitter()
+ log(`Network ID: ${yl(netId)}`)
+ log(`Deployer: ${yl(deployer)}`)
+
+ const state = readNetworkState(network.name, netId)
+ const srcAppName = APP_NAMES.NODE_OPERATORS_REGISTRY
+ assertRequiredNetworkState(state, REQUIRED_NET_STATE.concat([`app:${srcAppName}`, `app:${trgAppName}`]))
+
+ const kernelAddress = state.daoAddress || readStateAppAddress(state, `aragon-kernel`)
+ if (!kernelAddress) {
+ throw new Error(`No Aragon kernel (DAO address) found!`)
+ }
+
+ log.splitter()
+
+ log(`Using ENS:`, yl(state.ensAddress))
+ const ens = await artifacts.require('ENS').at(state.ensAddress)
+ log.splitter()
+
+ const srcAppFullName = `${srcAppName}.${state.lidoApmEnsName}`
+ const srcAppId = namehash(srcAppFullName)
+ const { semanticVersion, contractAddress } = await resolveLatestVersion(srcAppId, ens, artifacts)
+ const srcVersion = semanticVersion.map((n) => n.toNumber())
+
+ log(`Source App:`, yl(srcAppName))
+ log(`Source App ENS:`, yl(srcAppFullName))
+ log(`Source App ID:`, yl(srcAppId))
+ log(`Source Contract implementation:`, yl(contractAddress))
+ log(`Source App version:`, yl(srcVersion.join('.')))
+ log.splitter()
+
+ const trgAppFullName = `${trgAppName}.${state.lidoApmEnsName}`
+ const trgAppId = namehash(trgAppFullName)
+ const trgProxyAddress = readStateAppAddress(state, `app:${trgAppName}`)
+ const trgAppArtifact = APP_ARTIFACTS[srcAppName] // get source app artifact
+ const trgApp = await artifacts.require(trgAppArtifact).at(trgProxyAddress)
+
+ // set new version to 1.0.0
+ const trgVersion = [1, 0, 0]
+ const contentURI = '0x' + Buffer.from(`ipfs:${ipfsCid}`, 'utf8').toString('hex')
+
+ log(`Target App:`, yl(trgAppName))
+ log(`Target App ENS:`, yl(trgAppFullName))
+ log(`Target App ID:`, yl(trgAppId))
+ log(`Target App proxy`, yl(trgProxyAddress))
+ log(`Target Contract implementation:`, yl(contractAddress))
+ log(`Target Content IPFS CID:`, yl(ipfsCid))
+ log(`Target Content URI:`, yl(contentURI))
+ log(`Target App version:`, yl(trgVersion.join('.')))
+
+ log.splitter()
+ const {
+ moduleName,
+ moduleType,
+ targetShare,
+ moduleFee,
+ treasuryFee,
+ penaltyDelay,
+ easyTrackAddress,
+ easyTrackTrustedCaller,
+ easyTrackFactories = {},
+ } = state[`app:${trgAppName}`].stakingRouterModuleParams
+
+ _checkLog(moduleName, `Target SR Module name`)
+ _checkLog(moduleType, `Target SR Module type`)
+ _checkLog(moduleFee, `Target SR Module fee`)
+ _checkLog(targetShare, `Target SR Module targetShare`)
+ _checkLog(treasuryFee, `Target SR Module treasuryFee`)
+ _checkLog(penaltyDelay, `Target SR Module penaltyDelay`)
+
+ if (!trgProxyAddress || (await web3.eth.getCode(trgProxyAddress)) === '0x') {
+ log.error(`Target app proxy is not yet deployed!`)
+ return
+ }
+
+ const trgRepoAddress = await resolveEnsAddress(artifacts, ens, trgAppId)
+
+ if (trgRepoAddress && (await web3.eth.getCode(trgRepoAddress)) !== '0x') {
+ log(`Target App APM repo:`, yl(trgRepoAddress))
+ log.error(`Target app is already deployed!`)
+ return
+ }
+
+ const lidoLocatorAddress = readStateAppAddress(state, `lidoLocator`)
+ const votingAddress = readStateAppAddress(state, `app:${APP_NAMES.ARAGON_VOTING}`)
+ const tokenManagerAddress = readStateAppAddress(state, `app:${APP_NAMES.ARAGON_TOKEN_MANAGER}`)
+ const srAddress = readStateAppAddress(state, 'stakingRouter')
+ const lidoApmAddress = readStateAppAddress(state, 'lidoApm')
+
+ const kernel = await artifacts.require('Kernel').at(kernelAddress)
+ const aclAddress = await kernel.acl()
+ const acl = await artifacts.require('ACL').at(aclAddress)
+ const stakingRouter = await artifacts.require('StakingRouter').at(srAddress)
+ const apmRegistry = await artifacts.require('APMRegistry').at(lidoApmAddress)
+
+ const voteDesc = `Clone app '${srcAppName}' to '${trgAppName}'`
+ const voting = await artifacts.require('Voting').at(votingAddress)
+ const tokenManager = await artifacts.require('TokenManager').at(tokenManagerAddress)
+ const agentAddress = readStateAppAddress(state, `app:${APP_NAMES.ARAGON_AGENT}`)
+ const agent = await artifacts.require('Agent').at(agentAddress)
+ const daoTokenAddress = await tokenManager.token()
+ const daoToken = await artifacts.require('MiniMeToken').at(daoTokenAddress)
+
+ const burnerAddress = readStateAppAddress(state, `burner`)
+ const burner = await artifacts.require('Burner').at(burnerAddress)
+
+ log.splitter()
+ log(`DAO Kernel`, yl(kernelAddress))
+ log(`ACL`, yl(aclAddress))
+ log(`Voting`, yl(votingAddress))
+ log(`Token manager`, yl(tokenManagerAddress))
+ log(`LDO token`, yl(daoTokenAddress))
+ log(`Lido APM`, yl(lidoApmAddress))
+ log(`Staking Router`, yl(srAddress))
+ log(`Burner`, yl(burnerAddress))
+ log(`Lido Locator:`, yl(lidoLocatorAddress))
+
+ log.splitter()
+
+ // use ethers.js Contract instance
+ const easytrack = new Contract(easyTrackAddress, easyTrackABI).connect(ethers.provider)
+ const easyTrackEVMScriptExecutor = await easytrack.evmScriptExecutor()
+
+ log(`EasyTrack`, yl(easyTrackAddress))
+ log(`EasyTrack EVM Script Executor`, yl(easyTrackEVMScriptExecutor))
+ log(`EasyTrack Trusted caller`, yl(easyTrackTrustedCaller))
+
+ for (const f of Object.keys(easyTrackFactories)) {
+ log(`EasyTrack Factory <${cy(f)}>`, yl(easyTrackFactories[f]))
+ const fc = new Contract(easyTrackFactories[f], easyTrackFactoryABI, ethers.provider)
+ _checkEqLog(await fc.trustedCaller(), easyTrackTrustedCaller, `EasyTrack Factory <${cy(f)}> trusted caller`)
+ }
+
+ log.splitter()
+ log(yl('^^^ check all the params above ^^^'))
+ await _pause()
+ log.splitter()
+
+ const evmScriptCalls = [
+ // create app repo
+ {
+ to: apmRegistry.address,
+ calldata: await apmRegistry.contract.methods
+ .newRepoWithVersion(trgAppName, votingAddress, trgVersion, contractAddress, contentURI)
+ .encodeABI(),
+ },
+ // link appId with implementations
+ {
+ to: kernel.address,
+ calldata: await kernel.contract.methods.setApp(KERNEL_APP_BASES_NAMESPACE, trgAppId, contractAddress).encodeABI(),
+ },
+ // initialize module
+ {
+ to: trgApp.address,
+ calldata: await trgApp.contract.methods
+ .initialize(lidoLocatorAddress, '0x' + Buffer.from(moduleType).toString('hex').padEnd(64, '0'), penaltyDelay)
+ .encodeABI(),
+ },
+ ]
+
+ // set permissions
+
+ // grant perm for staking router
+ evmScriptCalls.push({
+ to: acl.address,
+ calldata: await acl.contract.methods
+ .createPermission(srAddress, trgProxyAddress, STAKING_ROUTER_ROLE, votingAddress)
+ .encodeABI(),
+ })
+
+ // grant perms to easytrack evm script executor
+ evmScriptCalls.push({
+ to: acl.address,
+ calldata: await acl.contract.methods
+ .grantPermission(easyTrackEVMScriptExecutor, trgProxyAddress, STAKING_ROUTER_ROLE)
+ .encodeABI(),
+ })
+
+ evmScriptCalls.push({
+ to: acl.address,
+ calldata: await acl.contract.methods
+ .createPermission(easyTrackEVMScriptExecutor, trgProxyAddress, MANAGE_NODE_OPERATOR_ROLE, votingAddress)
+ .encodeABI(),
+ })
+ evmScriptCalls.push({
+ to: acl.address,
+ calldata: await acl.contract.methods
+ .createPermission(easyTrackEVMScriptExecutor, trgProxyAddress, SET_NODE_OPERATOR_LIMIT_ROLE, votingAddress)
+ .encodeABI(),
+ })
+
+ // grant manager to easytrack evm script executor
+ evmScriptCalls.push({
+ to: acl.address,
+ calldata: await acl.contract.methods
+ .createPermission(easyTrackEVMScriptExecutor, trgProxyAddress, MANAGE_SIGNING_KEYS, easyTrackEVMScriptExecutor)
+ .encodeABI(),
+ })
+
+ // grant perms to easytrack factories
+ evmScriptCalls.push({
+ to: easytrack.address,
+ calldata: await easytrack.interface.encodeFunctionData('addEVMScriptFactory', [
+ easyTrackFactories.AddNodeOperators,
+ trgProxyAddress +
+ getSignature(trgApp, 'addNodeOperator').substring(2) +
+ aclAddress.substring(2) +
+ getSignature(acl, 'grantPermissionP').substring(2),
+ ]),
+ })
+ evmScriptCalls.push({
+ to: easytrack.address,
+ calldata: await easytrack.interface.encodeFunctionData('addEVMScriptFactory', [
+ easyTrackFactories.ActivateNodeOperators,
+ trgProxyAddress +
+ getSignature(trgApp, 'activateNodeOperator').substring(2) +
+ aclAddress.substring(2) +
+ getSignature(acl, 'grantPermissionP').substring(2),
+ ]),
+ })
+ evmScriptCalls.push({
+ to: easytrack.address,
+ calldata: await easytrack.interface.encodeFunctionData('addEVMScriptFactory', [
+ easyTrackFactories.DeactivateNodeOperators,
+ trgProxyAddress +
+ getSignature(trgApp, 'deactivateNodeOperator').substring(2) +
+ aclAddress.substring(2) +
+ getSignature(acl, 'revokePermission').substring(2),
+ ]),
+ })
+ evmScriptCalls.push({
+ to: easytrack.address,
+ calldata: await easytrack.interface.encodeFunctionData('addEVMScriptFactory', [
+ easyTrackFactories.SetVettedValidatorsLimits,
+ trgProxyAddress + getSignature(trgApp, 'setNodeOperatorStakingLimit').substring(2),
+ ]),
+ })
+ evmScriptCalls.push({
+ to: easytrack.address,
+ calldata: await easytrack.interface.encodeFunctionData('addEVMScriptFactory', [
+ easyTrackFactories.UpdateTargetValidatorLimits,
+ trgProxyAddress + getSignature(trgApp, 'updateTargetValidatorsLimits').substring(2),
+ ]),
+ })
+ evmScriptCalls.push({
+ to: easytrack.address,
+ calldata: await easytrack.interface.encodeFunctionData('addEVMScriptFactory', [
+ easyTrackFactories.SetNodeOperatorNames,
+ trgProxyAddress + getSignature(trgApp, 'setNodeOperatorName').substring(2),
+ ]),
+ })
+ evmScriptCalls.push({
+ to: easytrack.address,
+ calldata: await easytrack.interface.encodeFunctionData('addEVMScriptFactory', [
+ easyTrackFactories.SetNodeOperatorRewardAddresses,
+ trgProxyAddress + getSignature(trgApp, 'setNodeOperatorRewardAddress').substring(2),
+ ]),
+ })
+ evmScriptCalls.push({
+ to: easytrack.address,
+ calldata: await easytrack.interface.encodeFunctionData('addEVMScriptFactory', [
+ easyTrackFactories.ChangeNodeOperatorManagers,
+ aclAddress +
+ getSignature(acl, 'revokePermission').substring(2) +
+ aclAddress.substring(2) +
+ getSignature(acl, 'grantPermissionP').substring(2),
+ ]),
+ })
+
+ // check missed STAKING_MODULE_MANAGE_ROLE role on Agent
+ if (!(await stakingRouter.hasRole(STAKING_MODULE_MANAGE_ROLE, voting.address))) {
+ evmScriptCalls.push({
+ to: agent.address,
+ calldata: await agent.contract.methods
+ .execute(
+ stakingRouter.address,
+ 0,
+ await stakingRouter.contract.methods.grantRole(STAKING_MODULE_MANAGE_ROLE, agent.address).encodeABI()
+ )
+ .encodeABI(),
+ })
+ }
+
+ // allow to request burner, add REQUEST_BURN_SHARES_ROLE
+ evmScriptCalls.push({
+ to: agent.address,
+ calldata: await agent.contract.methods
+ .execute(
+ burner.address,
+ 0,
+ await burner.contract.methods.grantRole(REQUEST_BURN_SHARES_ROLE, trgProxyAddress).encodeABI()
+ )
+ .encodeABI(),
+ })
+
+ // add module to SR
+ const addModuleCallData = await stakingRouter.contract.methods
+ .addStakingModule(
+ moduleName, // name
+ trgProxyAddress, // module address
+ targetShare,
+ moduleFee,
+ treasuryFee
+ )
+ .encodeABI()
+ evmScriptCalls.push({
+ to: agent.address,
+ calldata: await agent.contract.methods.execute(stakingRouter.address, 0, addModuleCallData).encodeABI(),
+ })
+
+ const evmScript = encodeCallScript(evmScriptCalls)
+
+ const evmScriptDecoder = new EVMScriptDecoder(
+ new abiProviders.Local({
+ [kernel.address]: kernel.abi,
+ [acl.address]: acl.abi,
+ [voting.address]: voting.abi,
+ [agent.address]: agent.abi,
+ [stakingRouter.address]: stakingRouter.abi,
+ [apmRegistry.address]: apmRegistry.abi,
+ [trgApp.address]: trgApp.abi,
+ [easytrack.address]: easyTrackABI,
+ })
+ )
+
+ const decodedEVMScript = await evmScriptDecoder.decodeEVMScript(evmScript)
+
+ log('Decoded voting script:')
+ for (const call of decodedEVMScript.calls) {
+ if (call.abi) {
+ const params = {}
+ const inputs = call.abi.inputs || []
+ for (let i = 0; i < inputs.length; ++i) {
+ params[inputs[i].name] = call.decodedCallData[i]
+ }
+ log({ contract: call.address, method: call.abi.name, params })
+ } else {
+ log(call)
+ }
+ }
+
+ const newVoteEvmScript = encodeCallScript([
+ {
+ to: voting.address,
+ calldata: await voting.contract.methods.newVote(evmScript, voteDesc, false, false).encodeABI(),
+ },
+ ])
+
+ // skip update if VOTE_ID set
+ if (!VOTE_ID) {
+ // save app info
+ persistNetworkState(network.name, netId, state, {
+ [`app:${trgAppName}`]: {
+ aragonApp: {
+ name: trgAppName,
+ fullName: trgAppFullName,
+ id: trgAppId,
+ ipfsCid,
+ contentURI,
+ },
+ implementation: {
+ address: contractAddress,
+ contract: 'contracts/0.4.24/nos/NodeOperatorsRegistry.sol',
+ },
+ },
+ })
+ }
+
+ log.splitter()
+ log(yl('^^^ check the decoded voting script above ^^^'))
+
+ if (SIMULATE) {
+ await _pause('Ready for simulation')
+ log.splitter()
+ log(gr(`Simulating voting creation and enact!`))
+ const { voters, quorum } = await getVoters(agentAddress, state.vestingParams, daoToken, voting)
+
+ let voteId
+ if (!VOTE_ID) {
+ // create voting on behalf ldo holder
+ await ethers.getImpersonatedSigner(voters[0])
+ log(`Creating voting on behalf holder`, yl(voters[0]))
+ const result = await tokenManager.forward(newVoteEvmScript, { from: voters[0], gasPrice: 0 })
+ voteId = getEventArgument(result, 'StartVote', 'voteId', { decodeForAbi: voting.abi })
+ log(`Voting created, Vote ID:`, yl(voteId))
+ } else {
+ voteId = VOTE_ID
+ }
+
+ // vote
+ log(`Checking state, Vote ID:`, yl(voteId))
+ let vote = await voting.getVote(voteId)
+ if (vote.executed) {
+ log.error(`Vote ID: ${yl(voteId)} is already executed, can't simulate!`)
+ return
+ }
+
+ log(`Collecting votes...`)
+ for (const voter of voters) {
+ if (vote.yea.gte(quorum)) {
+ break
+ }
+ const canVote = await voting.canVote(voteId, voter)
+
+ if (canVote) {
+ await ethers.getImpersonatedSigner(voter)
+ log(`Cast voting on behalf holder:`, yl(voter))
+
+ await voting.vote(voteId, true, true, { from: voter, gasPrice: 0 })
+ vote = await voting.getVote(voteId)
+ } else {
+ log(`Skip holder (can't vote):`, voter)
+ }
+ }
+
+ if (vote.yea.lt(quorum)) {
+ log.error(`Not enough voting power for Vote ID:`, yl(voteId))
+ return
+ }
+ log(`Vote quorum passed`)
+
+ const voteTime = (await voting.voteTime()).toNumber()
+ // pass time and enact
+ log(`Pass time...`)
+ await ethers.provider.send('evm_increaseTime', [voteTime])
+ await ethers.provider.send('evm_mine')
+ log(`Enacting vote...`)
+ await voting.executeVote(voteId, { from: deployer, gasPrice: 0 })
+
+ log(`Vote executed!`)
+ _checkEqLog(await trgApp.hasInitialized(), true, `Target App initialized`)
+ } else {
+ await _pause('Ready for TX')
+ log.splitter()
+
+ const tx = await log.tx(
+ `Voting: Clone app '${srcAppName}' to '${trgAppName}'`,
+ tokenManager.forward(newVoteEvmScript, { from: deployer })
+ )
+
+ const voteId = getEventArgument(tx, 'StartVote', 'voteId', { decodeForAbi: voting.abi })
+ log(`Voting created, id`, yl(voteId))
+ }
+ // else {
+ // await saveCallTxData(
+ // `Voting: Clone app '${srcAppName}' to '${trgAppName}'`,
+ // tokenManager,
+ // 'forward',
+ // `clone-tx-02-create-voting.json`,
+ // {
+ // arguments: [newVoteEvmScript],
+ // from: deployer,
+ // }
+ // )
+ // // console.log({ txData })
+
+ // log.splitter()
+ // log(gr(`Before continuing the cloning, please send voting creation transactions`))
+ // log(gr(`that you can find in the file listed above. You may use a multisig address`))
+ // log(gr(`if it supports sending arbitrary tx.`))
+ // }
+
+ log.splitter()
+}
+
+// try to get list of voters with most significant LDO amounts
+async function getVoters(agentAddress, vestingParams, daoToken, voting) {
+ const totalSupply = await daoToken.totalSupply()
+ const quorumPcnt = await voting.minAcceptQuorumPct()
+ const quorum = totalSupply.mul(quorumPcnt).div(toBN(ETH(1)))
+ const minBalance = quorum.div(toBN(10)) // cliff to skip small holders
+ const voters = []
+ let voteBalance = toBN(0)
+
+ const holders = [
+ agentAddress, // agent at 1st place as potentially the only sufficient
+ ...Object.entries(vestingParams.holders)
+ .sort((a, b) => (a[1] < b[1] ? 1 : a[1] > b[1] ? -1 : 0))
+ .map(([h, b]) => h),
+ ]
+
+ for (const holder of holders) {
+ const balance = await daoToken.balanceOf(holder)
+ if (balance.gte(minBalance)) {
+ voters.push(holder)
+ voteBalance = voteBalance.add(balance)
+ if (voteBalance.gt(quorum)) {
+ break
+ }
+ }
+ }
+
+ return { voters, quorum }
+}
+
+module.exports = runOrWrapScript(deploySimpleDVT, module)
diff --git a/scripts/simpledvt/03-check-deployed.js b/scripts/simpledvt/03-check-deployed.js
new file mode 100644
index 000000000..0ca2f0d15
--- /dev/null
+++ b/scripts/simpledvt/03-check-deployed.js
@@ -0,0 +1,489 @@
+const { network, ethers } = require('hardhat')
+const { Contract, utils } = require('ethers')
+const runOrWrapScript = require('../helpers/run-or-wrap-script')
+const { log, yl, gr, cy } = require('../helpers/log')
+const {
+ readStateAppAddress,
+ _checkEq,
+ _pause,
+ MANAGE_SIGNING_KEYS,
+ MANAGE_NODE_OPERATOR_ROLE,
+ SET_NODE_OPERATOR_LIMIT_ROLE,
+ STAKING_ROUTER_ROLE,
+ STAKING_MODULE_MANAGE_ROLE,
+ REQUEST_BURN_SHARES_ROLE,
+ SIMPLE_DVT_IPFS_CID,
+ easyTrackABI,
+ easyTrackEvmExecutorABI,
+ easyTrackFactoryABI,
+} = require('./helpers')
+const { readNetworkState, assertRequiredNetworkState } = require('../helpers/persisted-network-state')
+const { hash: namehash } = require('eth-ens-namehash')
+const { resolveLatestVersion } = require('../components/apm')
+const { APP_NAMES, APP_ARTIFACTS } = require('../constants')
+const { ETH, toBN, genKeys, ethToStr } = require('../../test/helpers/utils')
+const { EvmSnapshot } = require('../../test/helpers/blockchain')
+const { reportOracle, getSecondsPerFrame } = require('../../test/helpers/oracle')
+
+const APP_TRG = process.env.APP_TRG || 'simple-dvt'
+const APP_IPFS_CID = process.env.APP_IPFS_CID || SIMPLE_DVT_IPFS_CID
+
+const SIMULATE = !!process.env.SIMULATE
+
+const REQUIRED_NET_STATE = [
+ 'ensAddress',
+ 'lidoApm',
+ 'lidoApmEnsName',
+ 'lidoLocator',
+ `app:${APP_NAMES.ARAGON_AGENT}`,
+ `app:${APP_NAMES.ARAGON_VOTING}`,
+ `app:${APP_NAMES.ARAGON_TOKEN_MANAGER}`,
+]
+
+async function checkSimpleDVT({ web3, artifacts, trgAppName = APP_TRG, ipfsCid = APP_IPFS_CID }) {
+ const netId = await web3.eth.net.getId()
+
+ log.splitter()
+ log(`Network ID: ${yl(netId)}`)
+
+ const state = readNetworkState(network.name, netId)
+ assertRequiredNetworkState(state, REQUIRED_NET_STATE.concat([`app:${trgAppName}`]))
+
+ const kernelAddress = state.daoAddress || readStateAppAddress(state, `aragon-kernel`)
+ if (!kernelAddress) {
+ throw new Error(`No Aragon kernel (DAO address) found!`)
+ }
+
+ log.splitter()
+
+ log(`Using ENS:`, yl(state.ensAddress))
+ const ens = await artifacts.require('ENS').at(state.ensAddress)
+ const lidoLocatorAddress = readStateAppAddress(state, `lidoLocator`)
+ log(`Lido Locator:`, yl(lidoLocatorAddress))
+ log.splitter()
+
+ const srcAppName = APP_NAMES.NODE_OPERATORS_REGISTRY
+ const srcAppFullName = `${srcAppName}.${state.lidoApmEnsName}`
+ const srcAppId = namehash(srcAppFullName)
+ const { contractAddress: srcContractAddress } = await resolveLatestVersion(srcAppId, ens, artifacts)
+
+ const trgAppFullName = `${trgAppName}.${state.lidoApmEnsName}`
+ const trgAppId = namehash(trgAppFullName)
+
+ const { semanticVersion, contractAddress, contentURI } = await resolveLatestVersion(trgAppId, ens, artifacts)
+
+ _checkEq(contractAddress, srcContractAddress, 'App APM repo last version: implementation is the same to NOR')
+ _checkEq(
+ contentURI,
+ '0x' + Buffer.from(`ipfs:${ipfsCid}`, 'utf8').toString('hex'),
+ 'App APM repo last version: IPFS CIT correct'
+ )
+ _checkEq(semanticVersion.map((x) => x.toNumber()).join(''), '100', 'App APM repo last version: app version = 1.0.0')
+
+ const trgProxyAddress = readStateAppAddress(state, `app:${trgAppName}`)
+ const trgAppArtifact = APP_ARTIFACTS[srcAppName] // get source app artifact
+ const trgApp = await artifacts.require(trgAppArtifact).at(trgProxyAddress)
+ const {
+ moduleName,
+ moduleType,
+ targetShare,
+ moduleFee,
+ treasuryFee,
+ penaltyDelay,
+ easyTrackAddress,
+ easyTrackTrustedCaller,
+ easyTrackFactories = {},
+ } = state[`app:${trgAppName}`].stakingRouterModuleParams
+
+ _checkEq(await trgApp.appId(), trgAppId, 'App Contract: AppID correct')
+ _checkEq(await trgApp.kernel(), kernelAddress, 'App Contract: kernel address correct')
+ _checkEq(await trgApp.hasInitialized(), true, 'App Contract: initialized')
+ _checkEq(await trgApp.getLocator(), lidoLocatorAddress, 'App Contract: Locator address correct')
+
+ log.splitter()
+ const kernel = await artifacts.require('Kernel').at(kernelAddress)
+ const aclAddress = await kernel.acl()
+ const acl = await artifacts.require('ACL').at(aclAddress)
+ const agentAddress = readStateAppAddress(state, `app:${APP_NAMES.ARAGON_AGENT}`)
+ const votingAddress = readStateAppAddress(state, `app:${APP_NAMES.ARAGON_VOTING}`)
+ const lidoAddress = readStateAppAddress(state, `app:${APP_NAMES.LIDO}`)
+ const srAddress = readStateAppAddress(state, 'stakingRouter')
+ const dsmAddress = readStateAppAddress(state, 'depositSecurityModule')
+ const stakingRouter = await artifacts.require('StakingRouter').at(srAddress)
+ const burnerAddress = readStateAppAddress(state, `burner`)
+ const burner = await artifacts.require('Burner').at(burnerAddress)
+ const easytrack = new Contract(easyTrackAddress, easyTrackABI).connect(ethers.provider)
+ const easyTrackEVMScriptExecutor = await easytrack.evmScriptExecutor()
+
+ const accountingOracleAddress = readStateAppAddress(state, 'accountingOracle')
+ const oracle = await artifacts.require('AccountingOracle').at(accountingOracleAddress)
+ const hashConsensusAddress = readStateAppAddress(state, 'hashConsensusForAccountingOracle')
+ const consensus = await artifacts.require('HashConsensus').at(hashConsensusAddress)
+
+ _checkEq(
+ await stakingRouter.hasRole(STAKING_MODULE_MANAGE_ROLE, agentAddress),
+ true,
+ 'Agent has role: STAKING_MODULE_MANAGE_ROLE'
+ )
+
+ _checkEq(
+ await burner.hasRole(REQUEST_BURN_SHARES_ROLE, trgProxyAddress),
+ true,
+ 'App has role: REQUEST_BURN_SHARES_ROLE'
+ )
+
+ _checkEq(
+ await acl.getPermissionManager(trgProxyAddress, MANAGE_SIGNING_KEYS),
+ easyTrackEVMScriptExecutor,
+ 'EasyTrackEVMScriptExecutor is permission manager: MANAGE_SIGNING_KEYS'
+ )
+ _checkEq(
+ await acl.getPermissionManager(trgProxyAddress, MANAGE_NODE_OPERATOR_ROLE),
+ votingAddress,
+ 'Voting is permission manager: MANAGE_NODE_OPERATOR_ROLE'
+ )
+ _checkEq(
+ await acl.getPermissionManager(trgProxyAddress, SET_NODE_OPERATOR_LIMIT_ROLE),
+ votingAddress,
+ 'Voting is permission manager: SET_NODE_OPERATOR_LIMIT_ROLE'
+ )
+ _checkEq(
+ await acl.getPermissionManager(trgProxyAddress, STAKING_ROUTER_ROLE),
+ votingAddress,
+ 'Voting is permission manager: STAKING_ROUTER_ROLE'
+ )
+
+ _checkEq(
+ await acl.hasPermission(easyTrackEVMScriptExecutor, trgProxyAddress, MANAGE_SIGNING_KEYS),
+ true,
+ 'EasyTrackEVMScriptExecutor has permission: MANAGE_SIGNING_KEYS'
+ )
+ _checkEq(
+ await acl.hasPermission(easyTrackEVMScriptExecutor, trgProxyAddress, MANAGE_NODE_OPERATOR_ROLE),
+ true,
+ 'EasyTrackEVMScriptExecutor has permission: MANAGE_NODE_OPERATOR_ROLE'
+ )
+ _checkEq(
+ await acl.hasPermission(easyTrackEVMScriptExecutor, trgProxyAddress, SET_NODE_OPERATOR_LIMIT_ROLE),
+ true,
+ 'EasyTrackEVMScriptExecutor has permission: SET_NODE_OPERATOR_LIMIT_ROLE'
+ )
+
+ _checkEq(
+ await acl.hasPermission(easyTrackEVMScriptExecutor, trgProxyAddress, STAKING_ROUTER_ROLE),
+ true,
+ 'EasyTrackEVMScriptExecutor has permission: STAKING_ROUTER_ROLE'
+ )
+
+ _checkEq(
+ await acl.hasPermission(srAddress, trgProxyAddress, STAKING_ROUTER_ROLE),
+ true,
+ 'StakingRouter has permission: STAKING_ROUTER_ROLE'
+ )
+
+ log.splitter()
+
+ _checkEq(await stakingRouter.getStakingModulesCount(), 2, 'StakingRouter: modules count = 2')
+ const srModuleId = 2
+ _checkEq(
+ await stakingRouter.hasStakingModule(srModuleId),
+ true,
+ `StakingRouter: expected moduleId = ${srModuleId} exists`
+ )
+
+ const srModule = await stakingRouter.getStakingModule(srModuleId)
+ _checkEq(srModule.name, moduleName, `StakingRouter module: name = ${trgAppName}`)
+ _checkEq(srModule.stakingModuleAddress, trgProxyAddress, `StakingRouter module: address correct`)
+ _checkEq(srModule.treasuryFee, treasuryFee, `StakingRouter module: treasuryFee = ${treasuryFee}`)
+ _checkEq(srModule.stakingModuleFee, moduleFee, `StakingRouter module: moduleFee = ${moduleFee}`)
+ _checkEq(srModule.targetShare, targetShare, `StakingRouter module: targetShare = ${targetShare}`)
+
+ log.splitter()
+
+ _checkEq(await trgApp.getStuckPenaltyDelay(), penaltyDelay, `App params: penalty delay = ${penaltyDelay}`)
+ _checkEq(
+ await trgApp.getType(),
+ '0x' + Buffer.from(moduleType).toString('hex').padEnd(64, '0'),
+ `App params: module type = ${moduleType}`
+ )
+
+ _checkEq(await trgApp.getNodeOperatorsCount(), 0, `App initial values: no any operators (count = 0)`)
+ _checkEq(await trgApp.getActiveNodeOperatorsCount(), 0, `App initial values: no active operators (count = 0)`)
+ _checkEq(await trgApp.getNonce(), 0, `App initial values: nonce (keysOpIndex) = 0`)
+
+ const { totalExitedValidators, totalDepositedValidators, depositableValidatorsCount } =
+ await trgApp.getStakingModuleSummary()
+ _checkEq(totalExitedValidators, 0, `App initial values: totalExitedValidators = 0`)
+ _checkEq(totalDepositedValidators, 0, `App initial values: totalDepositedValidators = 0`)
+ _checkEq(depositableValidatorsCount, 0, `App initial values: depositableValidatorsCount = 0`)
+
+ log.splitter()
+
+ // hardcode ET EVM script executor and ET factory ABIs to avoid adding external ABI files to repo
+
+ const allFactories = await easytrack.getEVMScriptFactories()
+ // console.log(allFactories)
+
+ // create ET factories instances
+ const factories = Object.entries(easyTrackFactories).reduce(
+ (f, [n, a]) => ({ ...f, [n]: new Contract(a, easyTrackFactoryABI, ethers.provider) }),
+ {}
+ )
+
+ for (const name of Object.keys(factories)) {
+ // `EasyTrack Factory <${cy(f)}>`
+ log(`ET factory <${cy(name)}>:`)
+ _checkEq(allFactories.includes(factories[name].address), true, `- in global list`)
+ _checkEq(await easytrack.isEVMScriptFactory(factories[name].address), true, `- isEVMScriptFactory`)
+ _checkEq(await factories[name].nodeOperatorsRegistry(), trgProxyAddress, `- matches target App`)
+ _checkEq(await factories[name].trustedCaller(), easyTrackTrustedCaller, `- trusted caller`)
+ }
+
+ log.splitter()
+
+ if (SIMULATE) {
+ await _pause('Ready for simulation')
+ log.splitter()
+
+ log(gr(`Simulating adding keys and deposit!`))
+ const strangers = await web3.eth.getAccounts()
+
+ const abiCoder = new utils.AbiCoder()
+
+ log('Creating snapshot...')
+ const snapshot = new EvmSnapshot(ethers.provider)
+ await snapshot.make()
+
+ try {
+ const lido = await artifacts.require('Lido').at(lidoAddress)
+
+ await ethers.getImpersonatedSigner(easyTrackAddress)
+ const easyTrackSigner = await ethers.getSigner(easyTrackAddress)
+ const evmExecutor = new Contract(easyTrackEVMScriptExecutor, easyTrackEvmExecutorABI, easyTrackSigner)
+
+ const ADDRESS_1 = '0x0000000000000000000000000000000000000001'
+ const ADDRESS_2 = '0x0000000000000000000000000000000000000002'
+ const MANAGER_1 = '0x0000000000000000000000000000000000000011'
+ const MANAGER_2 = '0x0000000000000000000000000000000000000012'
+
+ const depositsCount = 2000
+ const op1keysAmount = 100
+ const op2keysAmount = 50
+ const keysAmount = op1keysAmount + op2keysAmount
+ if ((await trgApp.getNodeOperatorsCount()) < 1) {
+ // prepare node operators
+
+ // add 2 NO via ET
+ // equivalent of:
+ // await trgApp.addNodeOperator('op 1', ADDRESS_1, { from: easyTrackEVMScriptExecutor, gasPrice: 0 })
+ // await trgApp.addNodeOperator('op 2', ADDRESS_2, { from: easyTrackEVMScriptExecutor, gasPrice: 0 })
+
+ log(`Adding 2 operators via ET ${cy('AddNodeOperators')} factory...`)
+ let callData = abiCoder.encode(
+ // struct AddNodeOperatorInput {
+ // string name;
+ // address rewardAddress;
+ // address managerAddress;
+ // }
+ //
+ // uint256 nodeOperatorsCount, AddNodeOperatorInput[] memory nodeOperators
+ ['uint256', 'tuple(string,address,address)[]'],
+ [
+ 0,
+ [
+ ['op 1', ADDRESS_1, MANAGER_1],
+ ['op 2', ADDRESS_2, MANAGER_2],
+ ],
+ ]
+ )
+ let evmScript = await factories.AddNodeOperators.createEVMScript(easyTrackTrustedCaller, callData)
+ await evmExecutor.executeEVMScript(evmScript, { gasPrice: 0 })
+ _checkEq(await trgApp.getNodeOperatorsCount(), 2, `Module operators count = 2`)
+
+ // add keys to module for op1 (on behalf op1 reward addr)
+ log(`Adding ${op1keysAmount} keys for op1 (on behalf op1 reward addr)...`)
+ await ethers.getImpersonatedSigner(ADDRESS_1)
+ let keys = genKeys(op1keysAmount)
+ await trgApp.addSigningKeys(0, op1keysAmount, keys.pubkeys, keys.sigkeys, { from: ADDRESS_1, gasPrice: 0 })
+
+ // add keys to module for op2 (on behalf op2 manager)
+ log(`Adding ${op2keysAmount} keys for op1 (on behalf op2 manager)...`)
+ await ethers.getImpersonatedSigner(MANAGER_2)
+ keys = genKeys(op2keysAmount)
+ await trgApp.addSigningKeys(1, op2keysAmount, keys.pubkeys, keys.sigkeys, { from: MANAGER_2, gasPrice: 0 })
+
+ log('Checking operators initial state...')
+ let opInfo = await trgApp.getNodeOperator(0, true)
+ _checkEq(opInfo.totalAddedValidators, op1keysAmount, `NO 1 totalAddedValidators = ${op1keysAmount}`)
+ opInfo = await trgApp.getNodeOperator(1, true)
+ _checkEq(opInfo.totalAddedValidators, op2keysAmount, `NO 2 totalAddedValidators = ${op2keysAmount}`)
+
+ // increase keys limit via ET
+ // equivalent of:
+ // await trgApp.setNodeOperatorStakingLimit(0, op1keysAmount, { from: easyTrackEVMScriptExecutor, gasPrice: 0 })
+ // await trgApp.setNodeOperatorStakingLimit(1, op2keysAmount, { from: easyTrackEVMScriptExecutor, gasPrice: 0 })
+
+ log(`Increasing operator's vetted keys limit via ET ${cy('SetVettedValidatorsLimits')} factory...`)
+ callData = abiCoder.encode(
+ // struct VettedValidatorsLimitInput {
+ // uint256 nodeOperatorId;
+ // uint256 stakingLimit;
+ // }
+ //
+ // VettedValidatorsLimitInput[]
+ ['tuple(uint256,uint256)[]'],
+ [
+ [
+ [0, op1keysAmount],
+ [1, op2keysAmount],
+ ],
+ ]
+ )
+ evmScript = await factories.SetVettedValidatorsLimits.createEVMScript(easyTrackTrustedCaller, callData)
+ await evmExecutor.executeEVMScript(evmScript)
+ }
+
+ log(`Checking SimpleDVT module state in StakingRouter...`)
+ let summary = await trgApp.getStakingModuleSummary()
+ _checkEq(summary.totalDepositedValidators, 0, `Module totalDepositedValidators = 0`)
+ _checkEq(summary.depositableValidatorsCount, keysAmount, `Module depositableValidatorsCount = ${keysAmount}`)
+
+ const wqAddress = readStateAppAddress(state, 'withdrawalQueueERC721')
+ const withdrwalQueue = await artifacts.require('WithdrawalQueueERC721').at(wqAddress)
+
+ const unfinalizedStETH = await withdrwalQueue.unfinalizedStETH()
+ const ethToDeposit = toBN(ETH(32 * depositsCount))
+ let depositableEther = await lido.getDepositableEther()
+
+ log(`Depositable ETH ${yl(ethToStr(depositableEther))} ETH`)
+ log(`Need (${yl(ethToStr(ethToDeposit))} ETH to deposit ${yl(depositsCount)} keys`)
+
+ // simulate deposits by transfering ethers to Lido contract
+ if (depositableEther.lt(ethToDeposit)) {
+ log(`Simulating additional ETH submitting...`)
+ const bufferedEther = await lido.getBufferedEther()
+ const wqDebt = unfinalizedStETH.gt(bufferedEther) ? unfinalizedStETH.sub(bufferedEther) : toBN(0)
+ let ethToSubmit = ethToDeposit.add(wqDebt)
+
+ let i = 0
+ const minBalance = toBN(ETH(1))
+ while (!ethToSubmit.isZero() && i < strangers.length) {
+ const balance = toBN(await web3.eth.getBalance(strangers[i]))
+ if (balance.gt(minBalance)) {
+ let ethToTransfer = balance.sub(minBalance)
+ if (ethToTransfer.gt(ethToSubmit)) {
+ ethToTransfer = ethToSubmit
+ }
+ log(`- ${ethToStr(ethToTransfer)} ETH from stranger ${strangers[i]}...`)
+ await web3.eth.sendTransaction({ value: ethToTransfer, to: lido.address, from: strangers[i], gasPrice: 0 })
+ ethToSubmit = ethToSubmit.sub(ethToTransfer)
+ }
+ ++i
+ }
+ }
+ depositableEther = await lido.getDepositableEther()
+
+ _checkEq(
+ depositableEther.gte(ethToDeposit),
+ true,
+ `Enough depositable ${yl(ethToStr(depositableEther))} ETH to` +
+ ` deposit ${yl(depositsCount)} keys (${yl(ethToStr(ethToDeposit))} ETH)`
+ )
+
+ // get max deposits count from SR (according targetShare value)
+ //
+ // NOR module id = 1
+ // const maxDepositsCount1 = (await stakingRouter.getStakingModuleMaxDepositsCount(1, ethToDeposit)).toNumber()
+ // SimpleDVT module id = 2
+ const maxDepositsCount2 = (await stakingRouter.getStakingModuleMaxDepositsCount(2, ethToDeposit)).toNumber()
+ // console.log({maxDepositsCount1, maxDepositsCount2});
+
+ log(`Depositing ${depositsCount} keys (on behalf DSM)..`)
+ const trgModuleId = 2 // sr module id
+ await ethers.getImpersonatedSigner(dsmAddress)
+ await lido.deposit(depositsCount, trgModuleId, '0x', {
+ from: dsmAddress,
+ gasPrice: 0,
+ })
+ await ethers.provider.send('evm_increaseTime', [600])
+ await ethers.provider.send('evm_mine')
+
+ log(`Checking SimpleDVT module new state in StakingRouter...`)
+ summary = await trgApp.getStakingModuleSummary()
+ _checkEq(
+ summary.totalDepositedValidators,
+ maxDepositsCount2,
+ `Summary totalDepositedValidators = ${maxDepositsCount2}`
+ )
+ _checkEq(
+ summary.depositableValidatorsCount,
+ keysAmount - maxDepositsCount2,
+ `Summary depositableValidatorsCount = ${keysAmount - maxDepositsCount2}`
+ )
+
+ // as only 2 ops in module and each has 0 deposited keys before
+ const depositedKeysHalf = maxDepositsCount2 / 2
+ let op1DepositedKeys
+ let op2DepositedKeys
+ if (op1keysAmount < depositedKeysHalf) {
+ op1DepositedKeys = op1keysAmount
+ op2DepositedKeys = maxDepositsCount2 - op1DepositedKeys
+ } else if (op2keysAmount < depositedKeysHalf) {
+ op2DepositedKeys = op2keysAmount
+ op1DepositedKeys = maxDepositsCount2 - op2DepositedKeys
+ } else {
+ op1DepositedKeys = depositedKeysHalf
+ op2DepositedKeys = depositedKeysHalf
+ }
+
+ const op1 = await trgApp.getNodeOperator(0, false)
+ _checkEq(op1.totalAddedValidators, op1keysAmount, `op1 state: totalAddedValidators = ${op1keysAmount}`)
+ _checkEq(
+ op1.totalDepositedValidators,
+ op1DepositedKeys,
+ `op1 state: totalDepositedValidators = ${op1DepositedKeys}`
+ )
+
+ const op2 = await trgApp.getNodeOperator(1, false)
+ _checkEq(op2.totalAddedValidators, op2keysAmount, `op2 state: totalAddedValidators = ${op2keysAmount}`)
+ _checkEq(
+ op2.totalDepositedValidators,
+ op2DepositedKeys,
+ `op2 state: totalDepositedValidators = ${op2DepositedKeys}`
+ )
+
+ log(`Simulating Oracle report...`)
+ const stat = await lido.getBeaconStat()
+ const rewards = stat.beaconBalance.mul(toBN(5)).div(toBN(365000)) // 5% annual
+ const clBalance = stat.beaconBalance.add(toBN(maxDepositsCount2).mul(toBN(ETH(32)))).add(rewards)
+
+ // pass 1 frame
+ const secondsPerFrame = await getSecondsPerFrame(consensus)
+ await ethers.provider.send('evm_increaseTime', [secondsPerFrame])
+ await ethers.provider.send('evm_mine')
+ const members = await consensus.getMembers()
+ for (const member of members.addresses) {
+ await ethers.getImpersonatedSigner(member)
+ }
+
+ const bal11 = await lido.balanceOf(ADDRESS_1)
+ const bal21 = await lido.balanceOf(ADDRESS_2)
+
+ await reportOracle(consensus, oracle, {
+ numValidators: stat.depositedValidators,
+ clBalance,
+ })
+
+ const bal12 = await lido.balanceOf(ADDRESS_1)
+ const bal22 = await lido.balanceOf(ADDRESS_2)
+
+ _checkEq(bal12.gt(bal11), true, 'op1 got rewards')
+ _checkEq(bal22.gt(bal21), true, 'op2 got rewards')
+ } finally {
+ log('Reverting snapshot...')
+ await snapshot.rollback()
+ }
+ }
+}
+
+module.exports = runOrWrapScript(checkSimpleDVT, module)
diff --git a/scripts/simpledvt/helpers.js b/scripts/simpledvt/helpers.js
new file mode 100644
index 000000000..ecbe7868b
--- /dev/null
+++ b/scripts/simpledvt/helpers.js
@@ -0,0 +1,182 @@
+const readline = require('readline')
+const { assert } = require('chai')
+const { log, rd, mg, yl } = require('../helpers/log')
+
+const KERNEL_APP_BASES_NAMESPACE = '0xf1f3eb40f5bc1ad1344716ced8b8a0431d840b5783aea1fd01786bc26f35ac0f'
+
+const MANAGE_SIGNING_KEYS = '0x75abc64490e17b40ea1e66691c3eb493647b24430b358bd87ec3e5127f1621ee'
+const MANAGE_NODE_OPERATOR_ROLE = '0x78523850fdd761612f46e844cf5a16bda6b3151d6ae961fd7e8e7b92bfbca7f8'
+const SET_NODE_OPERATOR_LIMIT_ROLE = '0x07b39e0faf2521001ae4e58cb9ffd3840a63e205d288dc9c93c3774f0d794754'
+const STAKING_ROUTER_ROLE = '0xbb75b874360e0bfd87f964eadd8276d8efb7c942134fc329b513032d0803e0c6'
+const STAKING_MODULE_MANAGE_ROLE = '0x3105bcbf19d4417b73ae0e58d508a65ecf75665e46c2622d8521732de6080c48'
+const REQUEST_BURN_SHARES_ROLE = '0x4be29e0e4eb91f98f709d98803cba271592782e293b84a625e025cbb40197ba8'
+const SIMPLE_DVT_IPFS_CID = 'QmaSSujHCGcnFuetAPGwVW5BegaMBvn5SCsgi3LSfvraSo'
+
+const easyTrackABI = [
+ {
+ inputs: [],
+ name: 'evmScriptExecutor',
+ outputs: [{ internalType: 'contract IEVMScriptExecutor', name: '', type: 'address' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [
+ {
+ internalType: 'address',
+ name: '_evmScriptFactory',
+ type: 'address',
+ },
+ {
+ internalType: 'bytes',
+ name: '_permissions',
+ type: 'bytes',
+ },
+ ],
+ name: 'addEVMScriptFactory',
+ outputs: [],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+ {
+ inputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
+ name: 'evmScriptFactories',
+ outputs: [{ internalType: 'address', name: '', type: 'address' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [{ internalType: 'address', name: '', type: 'address' }],
+ name: 'evmScriptFactoryPermissions',
+ outputs: [{ internalType: 'bytes', name: '', type: 'bytes' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [],
+ name: 'getEVMScriptFactories',
+ outputs: [{ internalType: 'address[]', name: '', type: 'address[]' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+
+ {
+ inputs: [{ internalType: 'address', name: '_maybeEVMScriptFactory', type: 'address' }],
+ name: 'isEVMScriptFactory',
+ outputs: [{ internalType: 'bool', name: '', type: 'bool' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+]
+
+const easyTrackEvmExecutorABI = [
+ {
+ inputs: [{ internalType: 'bytes', name: '_evmScript', type: 'bytes' }],
+ name: 'executeEVMScript',
+ outputs: [{ internalType: 'bytes', name: '', type: 'bytes' }],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+]
+
+const easyTrackFactoryABI = [
+ {
+ inputs: [
+ { internalType: 'address', name: '_creator', type: 'address' },
+ { internalType: 'bytes', name: '_evmScriptCallData', type: 'bytes' },
+ ],
+ name: 'createEVMScript',
+ outputs: [{ internalType: 'bytes', name: '', type: 'bytes' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [],
+ name: 'nodeOperatorsRegistry',
+ outputs: [{ internalType: 'contract INodeOperatorsRegistry', name: '', type: 'address' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [],
+ name: 'trustedCaller',
+ outputs: [{ internalType: 'address', name: '', type: 'address' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+]
+
+async function getDeployer(web3, defaultDeployer) {
+ if (!defaultDeployer) {
+ const [firstAccount] = await web3.eth.getAccounts()
+ return firstAccount
+ }
+ return defaultDeployer
+}
+
+function readStateAppAddress(state, app = '') {
+ const appState = state[app]
+ // goerli/mainnet deployed.json formats compatibility
+ return appState.proxyAddress || (appState.proxy && appState.proxy.address) || appState.address
+}
+
+function getSignature(instance, method) {
+ const methodAbi = instance.contract._jsonInterface.find((i) => i.name === method)
+ if (!methodAbi) {
+ throw new Error(`Method ${method} not found in contract`)
+ }
+ return methodAbi.signature
+}
+
+function _checkEq(a, b, descr = '') {
+ assert.equal(a, b, descr)
+ log.success(descr)
+}
+
+function _checkLog(value, msg) {
+ log(msg, yl(value))
+ assert.isDefined(value, 'Value is missing')
+}
+
+function _checkEqLog(value, etalon, msg) {
+ log(msg, yl(value))
+ assert.equal(value, etalon, `Value not equal to: ${etalon}`)
+}
+
+function _pause(msg) {
+ if (msg) log(rd(`!!! ${msg}`))
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
+ const query = mg('>>> Enter Y (or y) to continue, interrupt process otherwise:')
+
+ return new Promise((resolve) =>
+ rl.question(query, (ans) => {
+ rl.close()
+ if (ans !== 'y' && ans !== 'Y') {
+ console.error(rd('Process aborted'))
+ process.exit(1)
+ }
+ resolve()
+ })
+ )
+}
+
+module.exports = {
+ readStateAppAddress,
+ getDeployer,
+ getSignature,
+ _checkEq,
+ _checkLog,
+ _checkEqLog,
+ _pause,
+ KERNEL_APP_BASES_NAMESPACE,
+ MANAGE_SIGNING_KEYS,
+ MANAGE_NODE_OPERATOR_ROLE,
+ SET_NODE_OPERATOR_LIMIT_ROLE,
+ STAKING_ROUTER_ROLE,
+ STAKING_MODULE_MANAGE_ROLE,
+ REQUEST_BURN_SHARES_ROLE,
+ SIMPLE_DVT_IPFS_CID,
+ easyTrackABI,
+ easyTrackEvmExecutorABI,
+ easyTrackFactoryABI,
+}
diff --git a/test/0.4.24/clone-app.test.js b/test/0.4.24/clone-app.test.js
new file mode 100644
index 000000000..b8dc6c7be
--- /dev/null
+++ b/test/0.4.24/clone-app.test.js
@@ -0,0 +1,320 @@
+const { artifacts, contract, ethers, web3 } = require('hardhat')
+const { assert } = require('../helpers/assert')
+
+const { hash } = require('eth-ens-namehash')
+const { encodeCallScript } = require('@aragon/contract-helpers-test/src/aragon-os')
+const { getEventAt } = require('@aragon/contract-helpers-test')
+
+const { EvmSnapshot } = require('../helpers/blockchain')
+const { deployProtocol } = require('../helpers/protocol')
+const { createVote, enactVote } = require('../helpers/voting')
+const { setupNodeOperatorsRegistry, NodeOperatorsRegistry } = require('../helpers/staking-modules')
+const { padRight } = require('../helpers/utils')
+
+const StakingRouter = artifacts.require('StakingRouter')
+
+const {
+ lidoMockFactory,
+ oracleReportSanityCheckerStubFactory,
+ votingFactory,
+ hashConsensusFactory,
+ stakingRouterFactory,
+ addStakingModulesWrapper,
+ postSetup,
+} = require('../helpers/factories')
+
+// bytes32 0x63757261746564
+const CURATED_TYPE = padRight(web3.utils.fromAscii('curated'), 32)
+// const PENALTY_DELAY = 2 * 24 * 60 * 60 // 2 days
+
+const KERNEL_APP_BASES_NAMESPACE = '0xf1f3eb40f5bc1ad1344716ced8b8a0431d840b5783aea1fd01786bc26f35ac0f'
+
+contract('Simple DVT', ([appManager, , , , , , , , , , , , user1, user2, user3, nobody, depositor, treasury]) => {
+ let operators
+ let dao
+ let stakingRouter
+ let lidoLocator
+ let snapshot
+ let acl
+ let voting
+ let tokenManager
+ // let pool
+
+ before('deploy base app', async () => {
+ const deployed = await deployProtocol({
+ oracleReportSanityCheckerFactory: oracleReportSanityCheckerStubFactory,
+ lidoFactory: (protocol) => {
+ return lidoMockFactory({ ...protocol, voting: protocol.appManager })
+ },
+ stakingRouterFactory: (protocol) => {
+ return stakingRouterFactory({ ...protocol, voting: protocol.appManager })
+ },
+ hashConsensusFactory: (protocol) => {
+ return hashConsensusFactory({ ...protocol, voting: protocol.appManager })
+ },
+ postSetup: (protocol) => {
+ return postSetup({ ...protocol, voting: protocol.appManager })
+ },
+ addStakingModulesWrapper: (protocol, stakingModules) => {
+ return addStakingModulesWrapper({ ...protocol, voting: protocol.appManager }, stakingModules)
+ },
+ stakingModulesFactory: async (protocol) => {
+ const curatedModule = await setupNodeOperatorsRegistry({ ...protocol, voting: protocol.appManager }, false)
+
+ // await protocol.acl.grantPermission(
+ // protocol.appManager.address,
+ // curatedModule.address,
+ // await curatedModule.MANAGE_NODE_OPERATOR_ROLE()
+ // )
+ // await protocol.acl.grantPermission(
+ // protocol.appManager.address,
+ // curatedModule.address,
+ // await curatedModule.MANAGE_NODE_OPERATOR_ROLE()
+ // )
+
+ // await protocol.acl.grantPermission(
+ // protocol.appManager.address,
+ // protocol.stakingRouter.address,
+ // await protocol.stakingRouter.STAKING_MODULE_MANAGE_ROLE()
+ // )
+
+ await protocol.stakingRouter.grantRole(
+ await protocol.stakingRouter.STAKING_MODULE_MANAGE_ROLE(),
+ protocol.voting.address,
+ {
+ from: protocol.appManager.address,
+ }
+ )
+
+ return [
+ {
+ module: curatedModule,
+ name: 'SimpleDVT',
+ targetShares: 10000,
+ moduleFee: 500,
+ treasuryFee: 500,
+ },
+ ]
+ },
+ votingFactory,
+ depositSecurityModuleFactory: async () => {
+ return { address: depositor }
+ },
+ })
+
+ dao = deployed.dao
+ acl = deployed.acl
+ stakingRouter = deployed.stakingRouter
+ operators = deployed.stakingModules[0]
+ lidoLocator = deployed.lidoLocator
+ tokenManager = deployed.tokenManager
+ voting = deployed.voting
+ // pool = deployed.pool
+
+ snapshot = new EvmSnapshot(ethers.provider)
+ await snapshot.make()
+ })
+
+ afterEach(async () => {
+ await snapshot.rollback()
+ })
+
+ const newAppProxy = async (dao, appId) => {
+ const receipt = await dao.newAppProxy(dao.address, appId)
+
+ // Find the deployed proxy address in the tx logs.
+ const logs = receipt.logs
+ const log = logs.find((l) => l.event === 'NewAppProxy')
+ const proxyAddress = log.args.proxy
+
+ return proxyAddress
+ }
+
+ describe('clone NOR to simple-dvt', () => {
+ const cloneAppName = 'simple-dvt'
+ const cloneAppId = hash(`${cloneAppName}.aragonpm.test`)
+ let cloneAppProxyAddress
+ let cloneApp
+
+ const moduleName = 'SimpleDVT'
+ const penaltyDelay = 3600
+ const targetShare = 1000 // 10%
+ const moduleFee = 500
+ const treasuryFee = 500
+
+ let norAppId
+ let norBaseImpl
+
+ async function checkCloneModule(tx) {
+ const addEvent = getEventAt(tx, 'StakingModuleAdded', { decodeForAbi: StakingRouter.abi })
+
+ assert.equals(addEvent.args.stakingModuleId, 2)
+ assert.equals(addEvent.args.stakingModule.toLowerCase(), cloneApp.address.toLowerCase())
+ assert.equals(addEvent.args.name, moduleName)
+
+ assert.equals(await stakingRouter.getStakingModulesCount(), 2)
+
+ const moduleInfo = await stakingRouter.getStakingModule(2)
+ // assert.equals(moduleType, CURATED_TYPE)
+
+ assert.equals(moduleInfo.name, moduleName)
+ assert.equals(moduleInfo.stakingModuleAddress, cloneApp.address)
+ assert.equals(moduleInfo.stakingModuleFee, moduleFee)
+ assert.equals(moduleInfo.treasuryFee, treasuryFee)
+ assert.equals(moduleInfo.targetShare, targetShare)
+
+ const moduleSummary = await stakingRouter.getStakingModuleSummary(2)
+ assert.equals(moduleSummary.totalExitedValidators, 0)
+ assert.equals(moduleSummary.totalDepositedValidators, 0)
+ assert.equals(moduleSummary.depositableValidatorsCount, 0)
+ }
+
+ before(async () => {
+ norAppId = await operators.appId()
+ norBaseImpl = await dao.getApp(KERNEL_APP_BASES_NAMESPACE, norAppId)
+ })
+
+ it('manual clone', async () => {
+ // deploy stub proxy
+ cloneAppProxyAddress = await newAppProxy(dao, cloneAppId)
+ cloneApp = await NodeOperatorsRegistry.at(cloneAppProxyAddress)
+
+ // setup aragon app
+ await dao.setApp(KERNEL_APP_BASES_NAMESPACE, cloneAppId, norBaseImpl, { from: appManager })
+ assert.equal(await dao.getApp(KERNEL_APP_BASES_NAMESPACE, await cloneApp.appId()), norBaseImpl)
+
+ // initialize module
+ await cloneApp.initialize(lidoLocator.address, CURATED_TYPE, penaltyDelay, { from: nobody })
+ assert.equal(await cloneApp.getType(), CURATED_TYPE)
+ assert.equal(await cloneApp.getStuckPenaltyDelay(), penaltyDelay)
+
+ // set roles
+
+ await Promise.all([
+ // Allow voting to manage node operators registry
+ acl.createPermission(appManager, cloneApp.address, await operators.MANAGE_SIGNING_KEYS(), appManager, {
+ from: appManager,
+ }),
+ acl.createPermission(appManager, cloneApp.address, await operators.MANAGE_NODE_OPERATOR_ROLE(), appManager, {
+ from: appManager,
+ }),
+ acl.createPermission(appManager, cloneApp.address, await operators.SET_NODE_OPERATOR_LIMIT_ROLE(), appManager, {
+ from: appManager,
+ }),
+ acl.createPermission(
+ stakingRouter.address,
+ cloneApp.address,
+ await operators.STAKING_ROUTER_ROLE(),
+ appManager,
+ {
+ from: appManager,
+ }
+ ),
+ ])
+
+ // add to SR
+ const tx = await stakingRouter.addStakingModule(
+ moduleName, // name
+ cloneApp.address, // module name
+ targetShare,
+ moduleFee,
+ treasuryFee,
+ { from: appManager.address }
+ )
+
+ await checkCloneModule(tx)
+ })
+
+ it('via voting', async () => {
+ // deploy stub proxy
+ cloneAppProxyAddress = await newAppProxy(dao, cloneAppId)
+ cloneApp = await NodeOperatorsRegistry.at(cloneAppProxyAddress)
+
+ const evmScriptCalls = [
+ // {
+ // // registry.newRepoWithVersion(appName, aclGrantee, initialSemanticVersion, contractAddress, contentURI)
+ // to: apm.address,
+ // calldata: await apm.contract.methods
+ // .newRepoWithVersion(trgAppName, voting.address, version, contractAddress, contentURI)
+ // .encodeABI(),
+ // },
+ // setup aragon app
+ {
+ to: dao.address,
+ calldata: await dao.contract.methods.setApp(KERNEL_APP_BASES_NAMESPACE, cloneAppId, norBaseImpl).encodeABI(),
+ },
+ // initialize module
+ {
+ to: cloneApp.address,
+ calldata: await cloneApp.contract.methods
+ .initialize(lidoLocator.address, CURATED_TYPE, penaltyDelay)
+ .encodeABI(),
+ },
+
+ // set roles
+ {
+ to: acl.address,
+ calldata: await acl.contract.methods
+ .createPermission(voting.address, cloneApp.address, await operators.MANAGE_SIGNING_KEYS(), voting.address)
+ .encodeABI(),
+ },
+ {
+ to: acl.address,
+ calldata: await acl.contract.methods
+ .createPermission(
+ voting.address,
+ cloneApp.address,
+ await operators.MANAGE_NODE_OPERATOR_ROLE(),
+ voting.address
+ )
+ .encodeABI(),
+ },
+ {
+ to: acl.address,
+ calldata: await acl.contract.methods
+ .createPermission(
+ voting.address,
+ cloneApp.address,
+ await operators.SET_NODE_OPERATOR_LIMIT_ROLE(),
+ voting.address
+ )
+ .encodeABI(),
+ },
+ {
+ to: acl.address,
+ calldata: await acl.contract.methods
+ .createPermission(
+ stakingRouter.address,
+ cloneApp.address,
+ await operators.STAKING_ROUTER_ROLE(),
+ voting.address
+ )
+ .encodeABI(),
+ },
+
+ // add to SR
+ {
+ to: stakingRouter.address,
+ calldata: await stakingRouter.contract.methods
+ .addStakingModule(
+ moduleName, // name
+ cloneApp.address, // module name
+ targetShare,
+ moduleFee,
+ treasuryFee
+ )
+ .encodeABI(),
+ },
+ ]
+
+ const voteId = await createVote(voting, tokenManager, `Clone NOR`, encodeCallScript(evmScriptCalls), {
+ from: appManager,
+ })
+ await voting.vote(voteId, true, true, { from: appManager })
+
+ const tx = await enactVote(voting, voteId, { from: appManager })
+
+ await checkCloneModule(tx)
+ })
+ })
+})
diff --git a/test/0.8.9/sepolia-deposit-adapter.test.js b/test/0.8.9/sepolia-deposit-adapter.test.js
new file mode 100644
index 000000000..8c23890f1
--- /dev/null
+++ b/test/0.8.9/sepolia-deposit-adapter.test.js
@@ -0,0 +1,158 @@
+const { contract, artifacts, ethers } = require('hardhat')
+const { assert } = require('../helpers/assert')
+const { ETH } = require('../helpers/utils')
+
+const { EvmSnapshot } = require('../helpers/blockchain')
+
+const SepoliaDepositAdapter = artifacts.require('SepoliaDepositAdapter')
+const SepoliaDepositContract = artifacts.require('ISepoliaDepositContract')
+
+// To run Sepolia Deposit Adapter tests:
+// HARDHAT_FORKING_URL= HARDHAT_CHAIN_ID=11155111 npx hardhat test --grep "SepoliaDepositAdapter"
+contract('SepoliaDepositAdapter', ([deployer]) => {
+ let depositAdapter
+ let snapshot
+ let bepoliaToken
+ const sepoliaDepositContractAddress = '0x7f02C3E3c98b133055B8B348B2Ac625669Ed295D'
+ const EOAddress = '0x6885E36BFcb68CB383DfE90023a462C03BCB2AE5'
+ const bepoliaTokenHolder = EOAddress
+ // const log = console.log
+ const log = () => {}
+
+ before('deploy lido with dao', async function () {
+ const { chainId } = await ethers.provider.getNetwork()
+ if (chainId !== 11155111) {
+ return this.skip()
+ }
+
+ depositAdapter = await SepoliaDepositAdapter.new(sepoliaDepositContractAddress)
+ log('depositAdapter address', depositAdapter.address)
+
+ bepoliaToken = await ethers.getContractAt('ISepoliaDepositContract', sepoliaDepositContractAddress)
+
+ const code = await ethers.provider.getCode(depositAdapter.address)
+ assert.notEqual(code, '0x')
+
+ snapshot = new EvmSnapshot(ethers.provider)
+ await snapshot.make()
+ })
+
+ afterEach(async () => {
+ await snapshot.rollback()
+ })
+
+ describe('SepoliaDepositAdapter Logic', () => {
+ it(`recover Bepolia tokens`, async () => {
+ const adapterAddr = depositAdapter.address
+ const BEPOLIA_TO_TRANSFER = 2
+ const bepoliaHolderInitialBalance = await bepoliaToken.balanceOf(bepoliaTokenHolder)
+ const impersonatedSigner = await ethers.getImpersonatedSigner(bepoliaTokenHolder)
+
+ log('bepoliaHolderInitialBalance', bepoliaHolderInitialBalance)
+ await bepoliaToken.connect(impersonatedSigner).transfer(adapterAddr, BEPOLIA_TO_TRANSFER)
+
+ assert.equals(await bepoliaToken.balanceOf(adapterAddr), BEPOLIA_TO_TRANSFER)
+
+ const bepoliaHolderEndBalance = await bepoliaToken.balanceOf(bepoliaTokenHolder)
+ assert.equals(bepoliaHolderEndBalance, bepoliaHolderInitialBalance - BEPOLIA_TO_TRANSFER)
+ log('bepoliaHolderEndBalance', bepoliaHolderEndBalance)
+
+ // Recover Bepolia tokens
+ const receipt = await depositAdapter.recoverBepolia()
+ assert.emits(receipt, 'BepoliaRecovered', { amount: BEPOLIA_TO_TRANSFER })
+
+ const bepoliaTokensOnAdapter = await bepoliaToken.balanceOf(adapterAddr)
+ assert.equals(bepoliaTokensOnAdapter, 0)
+
+ const [owner] = await ethers.getSigners()
+ const bepoliaTokenHolderEnd = await bepoliaToken.balanceOf(owner.address)
+ assert.equals(bepoliaTokenHolderEnd, BEPOLIA_TO_TRANSFER)
+ })
+
+ it(`call deposit on Adapter`, async () => {
+ const key = '0x90823dc2e5ab8a52a0b32883ea8451cbe4c921a42ce439f4fb306a90e9f267e463241da7274b6d44c2e4b95ddbcb0ad3'
+ const withdrawalCredentials = '0x005bfe00d82068a0c2a6687afaf969dad5a9c663cb492815a65d203885aaf993'
+ const sig =
+ '0x802899068eb4b37c95d46869947cac42b9c65b90fcb3fde3854c93ad5737800c01e9c82e174c8ed5cc18210bd60a94ea0082a850817b1dddd4096059b6846417b05094c59d3dd7f4028ed9dff395755f9905a88015b0ed200a7ec1ed60c24922'
+ const dataRoot = '0x8b09ed1d0fb3b8e3bb8398c6b77ee3d8e4f67c23cb70555167310ef02b06e5f5'
+
+ const adapterAddr = depositAdapter.address
+
+ const balance0ETH = await ethers.provider.getBalance(adapterAddr)
+ assert.equals(balance0ETH, 0)
+
+ const impersonatedSigner = await ethers.getImpersonatedSigner(bepoliaTokenHolder)
+ // Transfer 1 Bepolia token to depositCaller
+ await bepoliaToken.connect(impersonatedSigner).transfer(adapterAddr, 1)
+
+ const [owner] = await ethers.getSigners()
+ log('owner', owner.address)
+
+ const bepoliaTokenHolderBalance = await bepoliaToken.balanceOf(bepoliaTokenHolder)
+ const adapterBepoliaBalance = await bepoliaToken.balanceOf(adapterAddr)
+ log('bepoliaTokenHolder and adapter balances: ', bepoliaTokenHolderBalance, adapterBepoliaBalance)
+ // We need to have exactly 1 Bepolia token in the adapter
+ assert.equals(adapterBepoliaBalance, 1)
+
+ const depositRootBefore = await depositAdapter.get_deposit_root()
+ log('depositRoot', depositRootBefore)
+ const depositCountBefore = await depositAdapter.get_deposit_count()
+ log('depositCount', depositCountBefore)
+
+ const sepoliaDepositContract = await SepoliaDepositContract.at(sepoliaDepositContractAddress)
+
+ const receipt = await depositAdapter.deposit(key, withdrawalCredentials, sig, dataRoot, {
+ from: owner.address,
+ value: ETH(32),
+ })
+ assert.emits(receipt, 'EthReceived', { sender: sepoliaDepositContractAddress, amount: ETH(32) })
+ const depositEvents = await sepoliaDepositContract.getPastEvents('DepositEvent')
+ assert.equals(depositEvents.length, 1)
+ log('depositEvents', depositEvents, ETH(32))
+
+ assert.equals(depositEvents[0].args.pubkey, key)
+ assert.equals(depositEvents[0].args.withdrawal_credentials, withdrawalCredentials)
+ assert.equals(depositEvents[0].args.signature, sig)
+
+ const depositRootAfter = await depositAdapter.get_deposit_root()
+ log('depositRoot After', depositRootAfter)
+ const depositCountAfter = await depositAdapter.get_deposit_count()
+ log('depositCount After', depositCountAfter)
+ assert.notEqual(depositRootBefore, depositRootAfter)
+ assert.equals(BigInt(depositCountBefore) + BigInt('0x0100000000000000'), BigInt(depositCountAfter))
+
+ const ethAfterDeposit = await ethers.provider.getBalance(adapterAddr)
+ log('ethAfterDeposit', ethAfterDeposit.toString())
+ assert.equals(ethAfterDeposit, 0)
+
+ const adapterBepoliaBalanceAfter = await bepoliaToken.balanceOf(adapterAddr)
+ assert.equals(adapterBepoliaBalanceAfter, 0)
+ })
+
+ it(`recover ETH`, async () => {
+ const ETH_TO_TRANSFER = ETH(10)
+ const adapterAddr = depositAdapter.address
+
+ const balance0ETH = await ethers.provider.getBalance(adapterAddr)
+ assert.equals(balance0ETH, 0)
+
+ const [owner] = await ethers.getSigners()
+ log('owner', owner.address)
+ await owner.sendTransaction({
+ to: adapterAddr,
+ value: ETH_TO_TRANSFER,
+ })
+
+ const ethAfterDeposit = await ethers.provider.getBalance(adapterAddr)
+ log('ethAfterDeposit', ethAfterDeposit.toString())
+ assert.equals(ethAfterDeposit, ETH_TO_TRANSFER)
+
+ const receipt = await depositAdapter.recoverEth()
+ assert.emits(receipt, 'EthRecovered', { amount: ETH_TO_TRANSFER })
+
+ const balanceEthAfterRecover = await ethers.provider.getBalance(adapterAddr)
+ log('balanceEthAfterRecover', balanceEthAfterRecover.toString())
+ assert.equals(balanceEthAfterRecover, 0)
+ })
+ })
+})
diff --git a/test/0.8.9/unstructured-storage.test.sol b/test/0.8.9/unstructured-storage.test.sol
new file mode 100644
index 000000000..557cc8a20
--- /dev/null
+++ b/test/0.8.9/unstructured-storage.test.sol
@@ -0,0 +1,149 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.9;
+
+import "forge-std/Test.sol";
+import {UnstructuredStorage} from "contracts/0.8.9/lib/UnstructuredStorage.sol";
+
+contract ExposedUnstructuredStorage {
+ function getStorageBool(bytes32 position) public view returns (bool) {
+ return UnstructuredStorage.getStorageBool(position);
+ }
+
+ function getStorageAddress(bytes32 position) public view returns (address) {
+ return UnstructuredStorage.getStorageAddress(position);
+ }
+
+ function getStorageBytes32(bytes32 position) public view returns (bytes32) {
+ return UnstructuredStorage.getStorageBytes32(position);
+ }
+
+ function getStorageUint256(bytes32 position) public view returns (uint256) {
+ return UnstructuredStorage.getStorageUint256(position);
+ }
+
+ function setStorageBool(bytes32 position, bool data) public {
+ return UnstructuredStorage.setStorageBool(position, data);
+ }
+
+ function setStorageAddress(bytes32 position, address data) public {
+ return UnstructuredStorage.setStorageAddress(position, data);
+ }
+
+ function setStorageBytes32(bytes32 position, bytes32 data) public {
+ return UnstructuredStorage.setStorageBytes32(position, data);
+ }
+
+ function setStorageUint256(bytes32 position, uint256 data) public {
+ return UnstructuredStorage.setStorageUint256(position, data);
+ }
+}
+
+contract ExposedUnstructuredStorageTest is Test {
+ ExposedUnstructuredStorage public unstructedStorage;
+
+ function setUp() public {
+ unstructedStorage = new ExposedUnstructuredStorage();
+ }
+
+ function testGetStorageBoolUnitialized() public {
+ bytes32 position = keccak256("FOO");
+ assertEq(unstructedStorage.getStorageBool(position), false);
+ }
+
+ function testGetStorageBoolUnitializedFuzz(bytes32 position) public {
+ assertEq(unstructedStorage.getStorageBool(position), false);
+ }
+
+ function testGetStorageAddressUnitialized() public {
+ bytes32 position = keccak256("FOO");
+ assertEq(unstructedStorage.getStorageAddress(position), address(0));
+ }
+
+ function testGetStorageAddressUnitializedFuzz(bytes32 position) public {
+ assertEq(unstructedStorage.getStorageAddress(position), address(0));
+ }
+
+ function testGetStorageBytes32Unitialized() public {
+ bytes32 position = keccak256("FOO");
+ bytes32 data;
+ assertEq(unstructedStorage.getStorageBytes32(position), data);
+ }
+
+ function testGetStorageBytes32UnitializedFuzz(bytes32 position) public {
+ bytes32 data;
+ assertEq(unstructedStorage.getStorageBytes32(position), data);
+ }
+
+ function testGetStorageUint256Unitialized() public {
+ bytes32 position = keccak256("FOO");
+ uint256 data;
+ assertEq(unstructedStorage.getStorageUint256(position), data);
+ }
+
+ function testGetStorageUint256UnitializedFuzz(bytes32 position) public {
+ uint256 data;
+ assertEq(unstructedStorage.getStorageUint256(position), data);
+ }
+
+ function testSetStorageBool() public {
+ bytes32 position = keccak256("FOO");
+ assertEq(unstructedStorage.getStorageBool(position), false);
+
+ unstructedStorage.setStorageBool(position, true);
+ assertEq(unstructedStorage.getStorageBool(position), true);
+
+ unstructedStorage.setStorageBool(position, false);
+ assertEq(unstructedStorage.getStorageBool(position), false);
+ }
+
+ function testSetStorageAddress() public {
+ bytes32 position = keccak256("FOO");
+ address data = vm.addr(1);
+
+ assertEq(unstructedStorage.getStorageAddress(position), address(0));
+ unstructedStorage.setStorageAddress(position, data);
+ assertEq(unstructedStorage.getStorageAddress(position), data);
+ }
+
+ function testSetStorageAddressFuzz(address data, bytes32 position) public {
+ assertEq(unstructedStorage.getStorageAddress(position), address(0));
+ unstructedStorage.setStorageAddress(position, data);
+ assertEq(unstructedStorage.getStorageAddress(position), data);
+ }
+
+ function testSetStorageBytes32() public {
+ bytes32 position = keccak256("FOO");
+ bytes32 data = keccak256("BAR");
+ bytes32 unintializedData;
+
+ assertEq(unstructedStorage.getStorageBytes32(position), unintializedData);
+ unstructedStorage.setStorageBytes32(position, data);
+ assertEq(unstructedStorage.getStorageBytes32(position), data);
+ }
+
+ function testSetStorageBytes32Fuzz(bytes32 data, bytes32 position) public {
+ bytes32 unintializedData;
+
+ assertEq(unstructedStorage.getStorageBytes32(position), unintializedData);
+ unstructedStorage.setStorageBytes32(position, data);
+ assertEq(unstructedStorage.getStorageBytes32(position), data);
+ }
+
+ function testSetStorageUint256() public {
+ bytes32 position = keccak256("FOO");
+ uint256 data = 1;
+ uint256 unintializedData;
+
+ assertEq(unstructedStorage.getStorageUint256(position), unintializedData);
+ unstructedStorage.setStorageUint256(position, data);
+ assertEq(unstructedStorage.getStorageUint256(position), data);
+ }
+
+ function testSetStorageUint256Fuzz(uint256 data, bytes32 position) public {
+ uint256 unintializedData;
+
+ assertEq(unstructedStorage.getStorageUint256(position), unintializedData);
+ unstructedStorage.setStorageUint256(position, data);
+ assertEq(unstructedStorage.getStorageUint256(position), data);
+ }
+}
diff --git a/test/helpers/voting.js b/test/helpers/voting.js
new file mode 100644
index 000000000..5f5c41bf5
--- /dev/null
+++ b/test/helpers/voting.js
@@ -0,0 +1,27 @@
+const { artifacts } = require('hardhat')
+const { getEventArgument } = require('@aragon/contract-helpers-test')
+const { encodeCallScript } = require('@aragon/contract-helpers-test/src/aragon-os')
+const { advanceChainTime } = require('./blockchain')
+const Voting = artifacts.require('Voting')
+
+async function createVote(voting, tokenManager, voteDesc, evmScript, txOpts) {
+ const newVoteEvmScript = encodeCallScript([
+ {
+ to: voting.address,
+ calldata: await voting.contract.methods.newVote(evmScript, voteDesc).encodeABI(),
+ },
+ ])
+ const tx = await tokenManager.forward(newVoteEvmScript, txOpts)
+ return getEventArgument(tx, 'StartVote', 'voteId', { decodeForAbi: Voting.abi })
+}
+
+async function enactVote(voting, voteId, txOpts) {
+ const voteTime = (await voting.voteTime()).toNumber()
+ await advanceChainTime(voteTime)
+ return await voting.executeVote(voteId, txOpts)
+}
+
+module.exports = {
+ createVote,
+ enactVote,
+}