diff --git a/accounts.sample.json b/accounts.sample.json
index a0540ca96..53b6a6e75 100644
--- a/accounts.sample.json
+++ b/accounts.sample.json
@@ -1,24 +1,12 @@
{
"eth": {
- "localhost": {
+ "dev-example": {
"mnemonic": "test test test test test test test test test test test junk",
"path": "m/44'/60'/0'/0",
"initialIndex": 0,
"count": 20
},
- "dev": {
- "mnemonic": "rich dune dash tag exercise veteran sword speed spike absorb disease brush bracket doll noodle",
- "path": "m/44'/60'/0'/0",
- "initialIndex": 0,
- "count": 20
- },
- "e2e": {
- "mnemonic": "explain tackle mirror kit van hammer degree position ginger unfair soup bonus",
- "path": "m/44'/60'/0'/0",
- "initialIndex": 0,
- "count": 50
- },
- "mainnet-fork-shapella-upgrade": ["4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d"]
+ "fork-example": ["ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"]
},
"infura": {
"projectId": "INFURA_PROJECT_ID"
diff --git a/apps/simple-dvt/README.md b/apps/simple-dvt/README.md
new file mode 100644
index 000000000..470755e46
--- /dev/null
+++ b/apps/simple-dvt/README.md
@@ -0,0 +1,82 @@
+# 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..fb71ccc9d
--- /dev/null
+++ b/apps/simple-dvt/app/public/meta/details.md
@@ -0,0 +1,6 @@
+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..ed4a68371
--- /dev/null
+++ b/apps/simple-dvt/arapp.json
@@ -0,0 +1,80 @@
+{
+ "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/deployed-goerli.json b/deployed-goerli.json
index 3ee3a3855..5ddec4953 100644
--- a/deployed-goerli.json
+++ b/deployed-goerli.json
@@ -131,6 +131,32 @@
"id": "0xb2977cfc13b000b6807b9ae3cf4d938f4cc8ba98e1d68ad911c58924d6aa4f11"
}
},
+ "app:simple-dvt": {
+ "stakingRouterModuleParams": {
+ "moduleName": "SimpleDVT",
+ "moduleType": "curated-onchain-v1",
+ "targetShare": 50,
+ "moduleFee": 800,
+ "treasuryFee": 200,
+ "penaltyDelay": 86400,
+ "easyTrackAddress": "0xAf072C8D368E4DD4A9d4fF6A76693887d6ae92Af",
+ "easyTrackFactories": {}
+ },
+ "aragonApp": {
+ "name": "simple-dvt",
+ "fullName": "simple-dvt.lidopm-testnet-prater.eth",
+ "id": "0x532a9a50bd35712df1229f81b7f2617f4d9f4f2f83b64aa83a3f4047010e246a"
+ },
+ "proxy": {
+ "address": "0x3f03E034D4fa3ab722A43b6064645Fe508e7140B",
+ "contract": "@aragon/os/contracts/apps/AppProxyUpgradeable.sol",
+ "constructorArgs": [
+ "0x1dD91b354Ebd706aB3Ac7c727455C7BAA164945A",
+ "0x532a9a50bd35712df1229f81b7f2617f4d9f4f2f83b64aa83a3f4047010e246a",
+ "0x"
+ ]
+ }
+ },
"aragonIDAddress": "0x673cB86B60B9768Eebb05517fbF5a7F9f1Ab62C4",
"burner": {
"deployParameters": {
@@ -429,4 +455,4 @@
],
"contract": "WstETH"
}
-}
\ No newline at end of file
+}
diff --git a/deployed-holesky.json b/deployed-holesky.json
index 3f44be7eb..8972660c8 100644
--- a/deployed-holesky.json
+++ b/deployed-holesky.json
@@ -175,22 +175,22 @@
"app:simple-dvt": {
"stakingRouterModuleParams": {
"moduleName": "SimpleDVT",
- "moduleType": "simple-dvt-onchain-v1",
+ "moduleType": "curated-onchain-v1",
"targetShare": 50,
"moduleFee": 800,
"treasuryFee": 200,
"penaltyDelay": 86400,
+ "easyTrackTrustedCaller": "0xD76001b33b23452243E2FDa833B6e7B8E3D43198",
"easyTrackAddress": "0x1763b9ED3586B08AE796c7787811a2E1bc16163a",
- "easyTrackEVMScriptExecutor": "0x2819B65021E13CEEB9AC33E77DB32c7e64e7520D",
"easyTrackFactories": {
- "AddNodeOperators": "0xC20129f1dd4DFeD023a6d6A8de9d54A7b61af5CC",
- "ActivateNodeOperators": "0x08c48Fef9Cadca882E27d2325D1785858D5c1aE3",
- "DeactivateNodeOperators": "0xf5436129Cf9d8fa2a1cb6e591347155276550635",
- "SetNodeOperatorNames": "0xb6a31141A579FCB540E3BB3504C58F1e6F5f543a",
- "SetNodeOperatorRewardAddresses": "0x7F9c5b838510e06b85DD146e71553EB7890fAf2e",
- "UpdateTargetValidatorLimits": "0x6e570D487aE5729Bd982A7bb3a7bfA5213AeAEdE",
- "SetVettedValidatorsLimits": "0xD420d6C8aA81c087829A64Ce59936b7C1176A81a",
- "TransferNodeOperatorManager": "0xaa49cF620e3f80Ce72D3A7668b1b4f3dF370D2C7"
+ "AddNodeOperators": "0xeF5233A5bbF243149E35B353A73FFa8931FDA02b",
+ "ActivateNodeOperators": "0x5b4A9048176D5bA182ceec8e673D8aA6927A40D6",
+ "DeactivateNodeOperators": "0x88d247cdf4ff4A4AAA8B3DD9dd22D1b89219FB3B",
+ "SetVettedValidatorsLimits": "0x30Cb36DBb0596aD9Cf5159BD2c4B1456c18e47E8",
+ "SetNodeOperatorNames": "0x4792BaC0a262200fA7d3b68e7622bFc1c2c3a72d",
+ "SetNodeOperatorRewardAddresses": "0x6Bfc576018C7f3D2a9180974E5c8e6CFa021f617",
+ "UpdateTargetValidatorLimits": "0xC91a676A69Eb49be9ECa1954fE6fc861AE07A9A2",
+ "ChangeNodeOperatorManagers": "0xb8C4728bc0826bA5864D02FA53148de7A44C2f7E"
}
},
"aragonApp": {
@@ -206,7 +206,14 @@
"0xe1635b63b5f7b5e545f2a637558a4029dea7905361a2f0fc28c66e9136cf86a4",
"0x"
]
- }
+ },
+ "fullName": "simple-dvt.lidopm.eth",
+ "name": "simple-dvt",
+ "id": "0xe1635b63b5f7b5e545f2a637558a4029dea7905361a2f0fc28c66e9136cf86a4",
+ "ipfsCid": "QmaSSujHCGcnFuetAPGwVW5BegaMBvn5SCsgi3LSfvraSo",
+ "contentURI": "0x697066733a516d615353756a484347636e4675657441504777565735426567614d42766e355343736769334c5366767261536f",
+ "implementation": "0xE0270CF2564d81E02284e16539F59C1B5a4718fE",
+ "contract": "NodeOperatorsRegistry"
},
"aragon-acl": {
"implementation": {
@@ -773,4 +780,4 @@
"0x3F1c547b21f65e10480dE3ad8E19fAAC46C95034"
]
}
-}
\ No newline at end of file
+}
diff --git a/deployed-mainnet.json b/deployed-mainnet.json
index 25007ce27..402e6734b 100644
--- a/deployed-mainnet.json
+++ b/deployed-mainnet.json
@@ -149,6 +149,48 @@
"contentURI": "0x697066733a516d656138394d5533504852503763513157616b3672327355654d554146324c39727132624c6d5963644b764c57"
}
},
+ "app:simple-dvt": {
+ "stakingRouterModuleParams": {
+ "moduleName": "SimpleDVT",
+ "moduleType": "curated-onchain-v1",
+ "targetShare": 50,
+ "moduleFee": 800,
+ "treasuryFee": 200,
+ "penaltyDelay": 432000,
+ "easyTrackTrustedCaller": "0x08637515E85A4633E23dfc7861e2A9f53af640f7",
+ "easyTrackAddress": "0xF0211b7660680B49De1A7E9f25C65660F0a13Fea",
+ "easyTrackFactories": {
+ "AddNodeOperators": "0xcAa3AF7460E83E665EEFeC73a7a542E5005C9639",
+ "ActivateNodeOperators": "0xCBb418F6f9BFd3525CE6aADe8F74ECFEfe2DB5C8",
+ "DeactivateNodeOperators": "0x8B82C1546D47330335a48406cc3a50Da732672E7",
+ "SetVettedValidatorsLimits": "0xD75778b855886Fc5e1eA7D6bFADA9EB68b35C19D",
+ "SetNodeOperatorNames": "0x7d509BFF310d9460b1F613e4e40d342201a83Ae4",
+ "SetNodeOperatorRewardAddresses": "0x589e298964b9181D9938B84bB034C3BB9024E2C0",
+ "UpdateTargetValidatorLimits": "0x41CF3DbDc939c5115823Fba1432c4EC5E7bD226C",
+ "ChangeNodeOperatorManagers": "0xE31A0599A6772BCf9b2bFc9e25cf941e793c9a7D"
+ }
+ },
+ "aragonApp": {
+ "name": "simple-dvt",
+ "fullName": "simple-dvt.lidopm.eth",
+ "id": "0xe1635b63b5f7b5e545f2a637558a4029dea7905361a2f0fc28c66e9136cf86a4",
+ "ipfsCid": "QmaSSujHCGcnFuetAPGwVW5BegaMBvn5SCsgi3LSfvraSo",
+ "contentURI": "0x697066733a516d615353756a484347636e4675657441504777565735426567614d42766e355343736769334c5366767261536f"
+ },
+ "proxy": {
+ "address": "0xaE7B191A31f627b4eB1d4DaC64eaB9976995b433",
+ "contract": "@aragon/os/contracts/apps/AppProxyUpgradeable.sol",
+ "constructorArgs": [
+ "0xb8FFC3Cd6e7Cf5a098A1c92F48009765B24088Dc",
+ "0xe1635b63b5f7b5e545f2a637558a4029dea7905361a2f0fc28c66e9136cf86a4",
+ "0x"
+ ]
+ },
+ "implementation": {
+ "address": "0x8538930c385C0438A357d2c25CB3eAD95Ab6D8ed",
+ "contract": "contracts/0.4.24/nos/NodeOperatorsRegistry.sol"
+ }
+ },
"aragon-kernel": {
"implementation": {
"contract": "@aragon/os/contracts/kernel/Kernel.sol",
diff --git a/hardhat.config.js b/hardhat.config.js
index 7ab85921c..05f606707 100644
--- a/hardhat.config.js
+++ b/hardhat.config.js
@@ -38,7 +38,7 @@ const getNetConfig = (networkName, ethAccountName) => {
const netState = readJson(`./deployed-${networkName}.json`) || {}
const ethAccts = accounts.eth || {}
- if (RPC_URL === undefined && networkName !== 'hardhat') {
+ if (RPC_URL === undefined && networkName !== 'hardhat' && networkName !== 'localhost') {
console.error('ERROR: RPC_URL env variable is not set')
process.exit(1)
}
@@ -52,6 +52,11 @@ const getNetConfig = (networkName, ethAccountName) => {
timeout: 60000,
}
const byNetName = {
+ localhost: {
+ ...base,
+ url: 'http://127.0.0.1:8545',
+ chainId: 31337,
+ },
mainnetfork: {
...base,
url: RPC_URL,
@@ -60,6 +65,11 @@ const getNetConfig = (networkName, ethAccountName) => {
...base,
url: RPC_URL,
},
+ holeskyfork: {
+ ...base,
+ url: RPC_URL,
+ chainId: Number(process.env.CHAIN_ID) || 17000,
+ },
local: {
url: RPC_URL,
},
diff --git a/package.json b/package.json
index 3716d0ebe..cbc2d9f39 100644
--- a/package.json
+++ b/package.json
@@ -79,6 +79,7 @@
"ethereumjs-testrpc-sc": "^6.5.1-sc.1",
"ethereumjs-util": "^7.0.8",
"ethers": "^5.1.4",
+ "evm-script-decoder": "git+https://github.com/lidofinance/evm-script-decoder.git#v0.2.2",
"ganache": "=7.6.0",
"hardhat": "2.12.7",
"hardhat-contract-sizer": "^2.5.0",
diff --git a/scripts/constants.js b/scripts/constants.js
index 05d866482..2703ca44e 100644
--- a/scripts/constants.js
+++ b/scripts/constants.js
@@ -4,6 +4,7 @@ const APP_NAMES = {
LIDO: 'lido',
ORACLE: 'oracle',
NODE_OPERATORS_REGISTRY: 'node-operators-registry',
+ SIMPLE_DVT: 'simple-dvt',
// Aragon apps
ARAGON_AGENT: 'aragon-agent',
ARAGON_FINANCE: 'aragon-finance',
diff --git a/scripts/helpers/persisted-network-state.js b/scripts/helpers/persisted-network-state.js
index c8a47fe9a..9feaf639b 100644
--- a/scripts/helpers/persisted-network-state.js
+++ b/scripts/helpers/persisted-network-state.js
@@ -10,6 +10,7 @@ const NETWORK_STATE_FILE_DIR = process.env.NETWORK_STATE_FILE_DIR || '.'
function readNetworkState(netName, netId) {
const fileName = _getFileName(netName, NETWORK_STATE_FILE_BASENAME, NETWORK_STATE_FILE_DIR)
const state = _readNetworkStateFile(fileName, netId)
+ assert.equal(netId, state.networkId, `Network id ${netId} does not match one in state file ${state.networkId}`)
return state
}
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/helpers/config.js b/test/helpers/config.js
index 451c89a02..3d127ecf2 100644
--- a/test/helpers/config.js
+++ b/test/helpers/config.js
@@ -64,6 +64,7 @@ const DEFAULT_FACTORIES = {
eip712StETHFactory: factories.eip712StETHFactory,
withdrawalCredentialsFactory: factories.withdrawalCredentialsFactory,
stakingModulesFactory: factories.stakingModulesFactory,
+ addStakingModulesWrapper: factories.addStakingModulesWrapper,
guardiansFactory: factories.guardiansFactory,
burnerFactory: factories.burnerFactory,
postSetup: factories.postSetup,
diff --git a/test/helpers/factories.js b/test/helpers/factories.js
index a68349244..1b31ae128 100644
--- a/test/helpers/factories.js
+++ b/test/helpers/factories.js
@@ -3,7 +3,7 @@ const withdrawals = require('./withdrawals')
const { newApp } = require('./dao')
const { artifacts } = require('hardhat')
const { deployLocatorWithDummyAddressesImplementation } = require('./locator-deploy')
-const { ETH } = require('./utils')
+const { ETH, ZERO_ADDRESS } = require('./utils')
const { SLOTS_PER_EPOCH, SECONDS_PER_SLOT, EPOCHS_PER_FRAME, CONSENSUS_VERSION } = require('./constants')
@@ -27,6 +27,9 @@ const Burner = artifacts.require('Burner')
const OracleReportSanityChecker = artifacts.require('OracleReportSanityChecker')
const ValidatorsExitBusOracle = artifacts.require('ValidatorsExitBusOracle')
const OracleReportSanityCheckerStub = artifacts.require('OracleReportSanityCheckerStub')
+const Voting = artifacts.require('Voting')
+const MiniMeToken = artifacts.require('MiniMeToken')
+const TokenManager = artifacts.require('TokenManager')
async function lidoMockFactory({ dao, appManager, acl, voting }) {
const base = await LidoMock.new()
@@ -74,7 +77,93 @@ async function appManagerFactory({ signers }) {
}
async function votingEOAFactory({ signers }) {
- return signers[1]
+ return { voting: signers[1], daoToken: null, tokenManager: null }
+}
+
+async function votingFactory({ appManager, dao, acl, deployParams }) {
+ const {
+ daoTokenDecimals = 18,
+ daoTokenName = 'DAO Token',
+ daoTokenSymbol = 'DAOTKN',
+ daoTokenTotalSupply = '1000000000000000000000000',
+ minSupportRequired = '500000000000000000',
+ minAcceptanceQuorum = '50000000000000000',
+ voteDuration = 3600,
+ objectionPhaseDuration = 1800,
+ } = deployParams
+
+ // deploy gov token (aka LDO)
+ const daoToken = await MiniMeToken.new(
+ ZERO_ADDRESS,
+ ZERO_ADDRESS,
+ 0,
+ daoTokenName,
+ daoTokenDecimals,
+ daoTokenSymbol,
+ true
+ )
+
+ // deploy TokenManager
+ const tmBase = await TokenManager.new()
+ const tmProxyAddress = await newApp(dao, 'aragon-token-manager', tmBase.address, appManager.address)
+ const tokenManager = await TokenManager.at(tmProxyAddress)
+ await daoToken.changeController(tokenManager.address, { from: appManager.address })
+ await tokenManager.initialize(daoToken.address, true, 0, { from: appManager.address })
+
+ await Promise.all([
+ acl.createPermission(
+ appManager.address,
+ tokenManager.address,
+ await tokenManager.ISSUE_ROLE(),
+ appManager.address,
+ {
+ from: appManager.address,
+ }
+ ),
+ acl.createPermission(
+ appManager.address,
+ tokenManager.address,
+ await tokenManager.ASSIGN_ROLE(),
+ appManager.address,
+ {
+ from: appManager.address,
+ }
+ ),
+ ])
+
+ // issue gov token to appManger
+ await tokenManager.issue(daoTokenTotalSupply, { from: appManager.address })
+ await tokenManager.assign(appManager.address, daoTokenTotalSupply, { from: appManager.address })
+
+ // deploy Voting
+ const votingBase = await Voting.new()
+ const proxyAddress = await newApp(dao, 'aragon-voting', votingBase.address, appManager.address)
+ const voting = await Voting.at(proxyAddress)
+ await voting.initialize(
+ daoToken.address,
+ minSupportRequired,
+ minAcceptanceQuorum,
+ voteDuration,
+ objectionPhaseDuration,
+ { from: appManager.address }
+ )
+
+ await Promise.all([
+ acl.grantPermission(voting.address, acl.address, await acl.CREATE_PERMISSIONS_ROLE(), {
+ from: appManager.address,
+ }),
+ acl.grantPermission(voting.address, dao.address, await dao.APP_MANAGER_ROLE(), {
+ from: appManager.address,
+ }),
+ acl.createPermission(tokenManager.address, voting.address, await voting.CREATE_VOTES_ROLE(), appManager.address, {
+ from: appManager.address,
+ }),
+ // acl.createPermission(voting.address, tokenManager.address, await tokenManager.ASSIGN_ROLE(), voting.address, {
+ // from: appManager.address,
+ // }),
+ ])
+
+ return { voting, daoToken, tokenManager }
}
async function treasuryFactory(_) {
@@ -287,6 +376,21 @@ async function stakingModulesFactory(_) {
return []
}
+async function addStakingModulesWrapper(protocol, stakingModules = []) {
+ for (const stakingModule of stakingModules) {
+ await protocol.stakingRouter.addStakingModule(
+ stakingModule.name,
+ stakingModule.module.address,
+ stakingModule.targetShares,
+ stakingModule.moduleFee,
+ stakingModule.treasuryFee,
+ { from: protocol.voting.address }
+ )
+ }
+
+ return stakingModules.map(({ module }) => module)
+}
+
async function guardiansFactory({ deployParams }) {
return {
privateKeys: deployParams.guardians,
@@ -399,6 +503,7 @@ module.exports = {
appManagerFactory,
treasuryFactory,
votingEOAFactory,
+ votingFactory,
depositContractFactory,
lidoMockFactory,
wstethFactory,
@@ -412,6 +517,7 @@ module.exports = {
eip712StETHFactory,
withdrawalCredentialsFactory,
stakingModulesFactory,
+ addStakingModulesWrapper,
guardiansFactory,
burnerFactory,
postSetup,
diff --git a/test/helpers/oracle.js b/test/helpers/oracle.js
index 56bfb988d..43ef2069e 100644
--- a/test/helpers/oracle.js
+++ b/test/helpers/oracle.js
@@ -69,9 +69,12 @@ async function prepareOracleReport({ clBalance, ...restFields }) {
async function triggerConsensusOnHash(hash, consensus) {
const members = await consensus.getMembers()
+ const quorum = (await consensus.getQuorum()).toNumber()
const { refSlot } = await consensus.getCurrentFrame()
- await consensus.submitReport(refSlot, hash, CONSENSUS_VERSION, { from: members.addresses[0] })
- await consensus.submitReport(refSlot, hash, CONSENSUS_VERSION, { from: members.addresses[1] })
+ for (let i = 0; i < quorum; i++) {
+ await consensus.submitReport(refSlot, hash, CONSENSUS_VERSION, { from: members.addresses[i] })
+ }
+ // await consensus.submitReport(refSlot, hash, CONSENSUS_VERSION, { from: members.addresses[1] })
assert.equal((await consensus.getConsensusState()).consensusReport, hash)
}
diff --git a/test/helpers/protocol.js b/test/helpers/protocol.js
index b338acef1..1449df1e1 100644
--- a/test/helpers/protocol.js
+++ b/test/helpers/protocol.js
@@ -13,13 +13,17 @@ async function deployProtocol(factories = {}, deployParams = {}) {
protocol.signers = await ethers.getSigners()
protocol.appManager = await protocol.factories.appManagerFactory(protocol)
protocol.treasury = await protocol.factories.treasuryFactory(protocol)
- protocol.voting = await protocol.factories.votingFactory(protocol)
- protocol.guardians = await protocol.factories.guardiansFactory(protocol)
const { dao, acl } = await newDao(protocol.appManager.address)
protocol.dao = dao
protocol.acl = acl
+ const { daoToken, tokenManager, voting } = await protocol.factories.votingFactory(protocol)
+ protocol.daoToken = daoToken
+ protocol.tokenManager = tokenManager
+ protocol.voting = voting
+
+ protocol.guardians = await protocol.factories.guardiansFactory(protocol)
protocol.pool = await protocol.factories.lidoFactory(protocol)
protocol.token = protocol.pool
protocol.wsteth = await protocol.factories.wstethFactory(protocol)
@@ -42,7 +46,10 @@ async function deployProtocol(factories = {}, deployParams = {}) {
protocol.withdrawalCredentials = await protocol.factories.withdrawalCredentialsFactory(protocol)
protocol.stakingRouter = await protocol.factories.stakingRouterFactory(protocol)
- protocol.stakingModules = await addStakingModules(protocol.factories.stakingModulesFactory, protocol)
+ protocol.stakingModules = await protocol.factories.addStakingModulesWrapper(
+ protocol,
+ await protocol.factories.stakingModulesFactory(protocol)
+ )
protocol.depositSecurityModule = await protocol.factories.depositSecurityModuleFactory(protocol)
protocol.elRewardsVault = await protocol.factories.elRewardsVaultFactory(protocol)
@@ -75,23 +82,6 @@ async function deployProtocol(factories = {}, deployParams = {}) {
return protocol
}
-async function addStakingModules(stakingModulesFactory, protocol) {
- const stakingModules = await stakingModulesFactory(protocol)
-
- for (const stakingModule of stakingModules) {
- await protocol.stakingRouter.addStakingModule(
- stakingModule.name,
- stakingModule.module.address,
- stakingModule.targetShares,
- stakingModule.moduleFee,
- stakingModule.treasuryFee,
- { from: protocol.voting.address }
- )
- }
-
- return stakingModules.map(({ module }) => module)
-}
-
module.exports = {
deployProtocol,
}
diff --git a/test/helpers/utils.js b/test/helpers/utils.js
index 736cdf7da..7afa05909 100644
--- a/test/helpers/utils.js
+++ b/test/helpers/utils.js
@@ -100,6 +100,7 @@ const shareRate = e27
const bnE9 = new BN(10).pow(new BN(9))
const ethToGwei = (valueEth) => toBN(valueEth).div(bnE9).toString()
+const ethToStr = (valueEth) => web3.utils.fromWei(toBN(valueEth), 'ether')
const changeEndianness = (string) => {
string = string.replace('0x', '')
@@ -202,6 +203,7 @@ module.exports = {
gwei,
ETH,
ethToGwei,
+ ethToStr,
StETH: ETH,
tokens,
changeEndianness,
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,
+}
diff --git a/yarn.lock b/yarn.lock
index 70a6c45de..0325b8859 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1926,7 +1926,7 @@ __metadata:
languageName: node
linkType: hard
-"@ethersproject/abi@npm:5.7.0, @ethersproject/abi@npm:^5.0.0-beta.146, @ethersproject/abi@npm:^5.0.9, @ethersproject/abi@npm:^5.7.0":
+"@ethersproject/abi@npm:5.7.0, @ethersproject/abi@npm:^5.0.0-beta.146, @ethersproject/abi@npm:^5.0.7, @ethersproject/abi@npm:^5.0.9, @ethersproject/abi@npm:^5.7.0":
version: 5.7.0
resolution: "@ethersproject/abi@npm:5.7.0"
dependencies:
@@ -3765,6 +3765,7 @@ __metadata:
ethereumjs-testrpc-sc: ^6.5.1-sc.1
ethereumjs-util: ^7.0.8
ethers: ^5.1.4
+ evm-script-decoder: "git+https://github.com/lidofinance/evm-script-decoder.git#v0.2.2"
ganache: =7.6.0
hardhat: 2.12.7
hardhat-contract-sizer: ^2.5.0
@@ -7729,7 +7730,7 @@ __metadata:
languageName: node
linkType: hard
-"bn.js@npm:^5.2.1":
+"bn.js@npm:^5.2.0, bn.js@npm:^5.2.1":
version: 5.2.1
resolution: "bn.js@npm:5.2.1"
checksum: 4693b52187524b856422b133cb2168ab5d593981891cb213e0565f5355008539f3887291f69c5ae2b254743c6c8d062ab7b69983e19bd00060023517d0a7ca8b
@@ -13182,6 +13183,16 @@ __metadata:
languageName: node
linkType: hard
+"evm-script-decoder@git+https://github.com/lidofinance/evm-script-decoder.git#v0.2.2":
+ version: 0.2.2
+ resolution: "evm-script-decoder@https://github.com/lidofinance/evm-script-decoder.git#commit=7bf9f89234e4504bf8246bdd3eb6cd43ad4c0709"
+ dependencies:
+ "@ethersproject/abi": ^5.0.7
+ keccak256: ^1.0.3
+ checksum: c1f9ca3c4cc0710e0a080493c6bac533611f0bd6ebb076c6928ced5fa82ab2782fce7d1ded250950b803f4be2c7a79315754bcd311d81a552e402c7b3a65916f
+ languageName: node
+ linkType: hard
+
"evp_bytestokey@npm:^1.0.0, evp_bytestokey@npm:^1.0.3":
version: 1.0.3
resolution: "evp_bytestokey@npm:1.0.3"
@@ -18187,6 +18198,17 @@ fsevents@~2.3.2:
languageName: node
linkType: hard
+"keccak256@npm:^1.0.3":
+ version: 1.0.6
+ resolution: "keccak256@npm:1.0.6"
+ dependencies:
+ bn.js: ^5.2.0
+ buffer: ^6.0.3
+ keccak: ^3.0.2
+ checksum: 5f7649021b30167c545dcf937aab5c93a2bb612f62722fa102e53d5c529feb3e4e4ab11baf7a2e419bf8d3bcc90a00fa6822e3095017fce2873679f2469c34ab
+ languageName: node
+ linkType: hard
+
"keccak@npm:3.0.1, keccak@npm:^3.0.0":
version: 3.0.1
resolution: "keccak@npm:3.0.1"
@@ -25289,6 +25311,42 @@ resolve@^1.22.1:
languageName: node
linkType: hard
+"simple-dvt-frontend@workspace:apps/simple-dvt/app":
+ version: 0.0.0-use.local
+ resolution: "simple-dvt-frontend@workspace:apps/simple-dvt/app"
+ dependencies:
+ "@aragon/api": ^2.0.0
+ "@aragon/api-react": ^2.0.0
+ "@aragon/ui": ^1.7.0
+ "@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
+ core-js: ^3.6.5
+ 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
+ formik: ^2.2.0
+ parcel-bundler: ^1.12.4
+ prettier: ^2.8.4
+ react: ^16.13.1
+ react-dom: ^16.13.1
+ regenerator-runtime: ^0.13.7
+ styled-components: ^5.2.0
+ yup: ^0.29.3
+ languageName: unknown
+ linkType: soft
+
"simple-get@npm:^2.7.0":
version: 2.8.1
resolution: "simple-get@npm:2.8.1"