Skip to content

Commit df95e56

Browse files
authored
Merge pull request #426 from lidofinance/develop
Feat: merge-ready protocol
2 parents 816bf1d + 6dc9c59 commit df95e56

File tree

322 files changed

+19803
-6397
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

322 files changed

+19803
-6397
lines changed

.github/assert-deployed-bytecode.js

+128
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
const { AssertionError } = require('chai')
2+
const chalk = require('chalk')
3+
const { web3 } = require('hardhat')
4+
const { readJSON } = require('../scripts/helpers/fs')
5+
const { APPS_TO_NAMES, CONTRACTS_TO_NAMES, IGNORE_METADATA_CONTRACTS } = require('./deployed-bytecode-consts')
6+
7+
const empty32bytes = '0000000000000000000000000000000000000000000000000000000000000000'
8+
9+
function isInsideEmpty32bytes(byteCode, index) {
10+
const start = index - 63 >= 0 ? index - 63 : 0
11+
const end = index + 64 <= byteCode.length ? index + 64 : byteCode.length
12+
return byteCode.slice(start, end).indexOf(empty32bytes) >= 0
13+
}
14+
15+
function stripMetadata(byteCode) {
16+
let metaDataLength = parseInt(byteCode.slice(-4), 16) * 2
17+
let metaDataIndex = byteCode.length - metaDataLength - 4
18+
if (metaDataIndex > 0) {
19+
return byteCode.slice(0, metaDataIndex)
20+
}
21+
return byteCode
22+
}
23+
24+
function isBytecodeSimilar(first, second, ignoreMetadata) {
25+
if (ignoreMetadata) {
26+
first = stripMetadata(first)
27+
second = stripMetadata(second)
28+
}
29+
if (first.length != second.length) {
30+
return false
31+
}
32+
for (var i = 0; i < first.length; i++) {
33+
if (first[i] != second[i] && !isInsideEmpty32bytes(first, i) && !isInsideEmpty32bytes(second, i)) {
34+
return false
35+
}
36+
}
37+
return true
38+
}
39+
40+
async function assertByteCode(address, artifactName, deployTx) {
41+
const artifact = await artifacts.readArtifact(artifactName)
42+
let bytecodeFromArtifact = artifact.deployedBytecode.toLowerCase()
43+
const bytecodeFromRpc = (await web3.eth.getCode(address)).toLowerCase()
44+
const ignoreMetadata = IGNORE_METADATA_CONTRACTS.includes(artifactName)
45+
if (bytecodeFromRpc === bytecodeFromArtifact) {
46+
console.log(chalk.green(`Compiled bytecode for ${chalk.yellow(address)}(${artifactName}) MATCHES deployed bytecode!`))
47+
} else if (isBytecodeSimilar(bytecodeFromArtifact, bytecodeFromRpc, ignoreMetadata)) {
48+
console.log(chalk.hex('#FFA500')(`Compiled bytecode for ${chalk.yellow(address)}(${artifactName}) is SIMILAR to deployed bytecode!`))
49+
if (deployTx) {
50+
await assertByteCodeByDeployTx(address, deployTx, artifact, ignoreMetadata)
51+
} else {
52+
throw new AssertionError(
53+
`No deployTx found for ${chalk.yellow(address)}(${artifactName}).\n` +
54+
`Double check is impossible, but required due to differences in the deployed bytecode`
55+
)
56+
}
57+
} else {
58+
throw new AssertionError(`Compiled bytecode for ${chalk.yellow(address)}(${artifactName}) DOESN'T MATCH deployed bytecode!`)
59+
}
60+
}
61+
62+
async function assertByteCodeByDeployTx(address, deployTx, artifact, ignoreMetadata) {
63+
const tx = await web3.eth.getTransaction(deployTx)
64+
const txData = tx.input.toLowerCase()
65+
const byteCode = ignoreMetadata ? stripMetadata(artifact.bytecode) : artifact.bytecode
66+
if (!txData.startsWith(byteCode)) {
67+
throw new AssertionError(
68+
`Bytecode from deploy TX DOESN'T MATCH compiled bytecode for ${chalk.yellow(address)}(${artifact.contractName})`
69+
)
70+
}
71+
console.log(
72+
chalk.green(
73+
`Bytecode from deploy TX ${ignoreMetadata ? 'SIMILAR to' : 'MATCHES'} compiled bytecode for ${chalk.yellow(address)}(${
74+
artifact.contractName
75+
})`
76+
)
77+
)
78+
}
79+
80+
async function assertDeployedByteCodeMain() {
81+
const deployInfo = await readJSON(`deployed-mainnet.json`)
82+
83+
// handle APPs
84+
const resultsApps = await Promise.allSettled(
85+
Object.entries(deployInfo).map(async ([key, value]) => {
86+
if (key.startsWith('app:') && !key.startsWith('app:aragon')) {
87+
const name = APPS_TO_NAMES.get(key.split(':')[1])
88+
if (!name) {
89+
throw `Unknown APP ${key}`
90+
}
91+
const address = value.baseAddress
92+
if (!address) {
93+
throw `APP ${key} has no baseAddress`
94+
}
95+
await assertByteCode(address, name)
96+
}
97+
})
98+
)
99+
// handle standalone contracts
100+
const resultsContracts = await Promise.allSettled(
101+
Object.entries(deployInfo).map(async ([key, value]) => {
102+
if (!key.startsWith('app:') && key.endsWith('Address')) {
103+
const name = CONTRACTS_TO_NAMES.get(key.replace('Address', ''))
104+
if (!name) {
105+
return
106+
}
107+
const address = value
108+
const deployTx = deployInfo[key.replace('Address', 'DeployTx')]
109+
await assertByteCode(address, name, deployTx)
110+
}
111+
})
112+
)
113+
let errors = []
114+
resultsApps.concat(resultsContracts).forEach((result) => {
115+
if (result.status == 'rejected') {
116+
errors.push(result.reason)
117+
}
118+
})
119+
if (errors.length > 0) {
120+
throw new Error(`Following errors occurred during execution:\n${chalk.red(errors.join('\n'))}`)
121+
}
122+
}
123+
124+
var myfunc = assertDeployedByteCodeMain()
125+
myfunc.catch((err) => {
126+
console.log(err)
127+
process.exit([1])
128+
})

.github/assert-git-changes.py

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
#!/usr/bin/python3
2+
import subprocess
3+
import os
4+
5+
target_dir = os.environ.get("TARGET_DIR")
6+
7+
if not target_dir:
8+
print("No TARGET_DIR env variable provided. Exiting")
9+
exit(1)
10+
11+
git_changes = subprocess.getoutput("git status --porcelain")
12+
print(f"Changes:\n{git_changes}")
13+
14+
if git_changes.find(target_dir) > 0:
15+
print(f"Changes in {target_dir} detected! Failing")
16+
exit(1)

.github/deployed-bytecode-consts.js

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
const APPS_TO_NAMES = new Map([
2+
['lido', 'Lido'],
3+
['node-operators-registry', 'NodeOperatorsRegistry'],
4+
['oracle', 'LidoOracle']
5+
])
6+
7+
const CONTRACTS_TO_NAMES = new Map([
8+
['wstethContract', 'WstETH'],
9+
['executionLayerRewardsVault', 'LidoExecutionLayerRewardsVault'],
10+
['compositePostRebaseBeaconReceiver', 'CompositePostRebaseBeaconReceiver'],
11+
['selfOwnedStETHBurner', 'SelfOwnedStETHBurner'],
12+
['depositor', 'DepositSecurityModule']
13+
])
14+
15+
const IGNORE_METADATA_CONTRACTS = ['WstETH']
16+
17+
module.exports = {
18+
APPS_TO_NAMES,
19+
CONTRACTS_TO_NAMES,
20+
IGNORE_METADATA_CONTRACTS
21+
}

.github/prepare-accounts-json.py

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
#!/usr/bin/python3
2+
from pathlib import Path
3+
import fileinput
4+
import shutil
5+
import os
6+
7+
INFURA_PROJECT_ID = os.environ.get("INFURA_PROJECT_ID")
8+
ETHERSCAN_API_KEY = os.environ.get("ETHERSCAN_API_KEY")
9+
10+
ACCOUNTS_TMPL = Path("./accounts.sample.json")
11+
ACCOUNTS = Path("./accounts.json")
12+
13+
14+
def main():
15+
shutil.copyfile(ACCOUNTS_TMPL, ACCOUNTS)
16+
with fileinput.FileInput(ACCOUNTS, inplace=True) as file:
17+
for line in file:
18+
updated_line = line.replace("INFURA_PROJECT_ID", INFURA_PROJECT_ID)
19+
updated_line = updated_line.replace("ETHERSCAN_API_KEY", ETHERSCAN_API_KEY)
20+
print(updated_line, end="")
21+
22+
23+
if __name__ == "__main__":
24+
main()

.github/workflows/assert-bytecode.yml

+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
name: CI
2+
3+
on:
4+
pull_request:
5+
branches:
6+
- 'master'
7+
8+
jobs:
9+
assert-bytecode:
10+
name: Assert deployed contracts bytecode
11+
runs-on: ubuntu-latest
12+
13+
steps:
14+
- uses: actions/checkout@v2
15+
16+
- name: Setup node.js version
17+
uses: actions/setup-node@v1
18+
with:
19+
node-version: 12.x
20+
21+
- name: Get yarn cache directory path
22+
id: yarn-cache-dir-path
23+
run: echo "::set-output name=dir::$(yarn config get cacheFolder)"
24+
25+
- name: Cache yarn cache
26+
id: cache-yarn-cache
27+
uses: actions/cache@v2
28+
with:
29+
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
30+
key: yarn-${{ hashFiles('**/yarn.lock') }}
31+
restore-keys: yarn-${{ hashFiles('**/yarn.lock') }}
32+
33+
- name: Cache node_modules
34+
id: cache-node-modules
35+
uses: actions/cache@v2
36+
with:
37+
path: '**/node_modules'
38+
key: node_modules-${{ hashFiles('**/yarn.lock') }}
39+
restore-keys: node_modules-${{ hashFiles('**/yarn.lock') }}
40+
41+
- name: Install modules
42+
run: yarn
43+
if: |
44+
steps.cache-yarn-cache.outputs.cache-hit != 'true' ||
45+
steps.cache-node-modules.outputs.cache-hit != 'true'
46+
47+
- name: Compile
48+
run: yarn compile
49+
50+
- name: Create accounts.json
51+
run: .github/prepare-accounts-json.py
52+
env:
53+
INFURA_PROJECT_ID: ${{ secrets.WEB3_INFURA_PROJECT_ID }}
54+
ETHERSCAN_API_KEY: ${{ secrets.ETHERSCAN_TOKEN }}
55+
56+
- name: Check deployed contract bytecode
57+
run: npx hardhat run .github/assert-deployed-bytecode.js
58+
env:
59+
NETWORK_NAME: mainnet

.github/workflows/fix-abi-pr.yml

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
name: Fix ABI
2+
3+
on:
4+
workflow_dispatch:
5+
6+
jobs:
7+
abi-fix-pr:
8+
name: Extract ABi and create PR
9+
runs-on: ubuntu-latest
10+
11+
steps:
12+
- uses: actions/checkout@v2
13+
14+
- name: Setup node.js version
15+
uses: actions/setup-node@v1
16+
with:
17+
node-version: 12.x
18+
19+
- name: Get yarn cache directory path
20+
id: yarn-cache-dir-path
21+
run: echo "::set-output name=dir::$(yarn config get cacheFolder)"
22+
23+
- name: Cache yarn cache
24+
id: cache-yarn-cache
25+
uses: actions/cache@v2
26+
with:
27+
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
28+
key: yarn-${{ hashFiles('**/yarn.lock') }}
29+
restore-keys: yarn-${{ hashFiles('**/yarn.lock') }}
30+
31+
- name: Cache node_modules
32+
id: cache-node-modules
33+
uses: actions/cache@v2
34+
with:
35+
path: '**/node_modules'
36+
key: node_modules-${{ hashFiles('**/yarn.lock') }}
37+
restore-keys: node_modules-${{ hashFiles('**/yarn.lock') }}
38+
39+
- name: Install modules
40+
run: yarn
41+
if: |
42+
steps.cache-yarn-cache.outputs.cache-hit != 'true' ||
43+
steps.cache-node-modules.outputs.cache-hit != 'true'
44+
45+
- name: Compile code and extract ABI
46+
run: yarn compile
47+
48+
- name: Check for ABI changes
49+
id: changes
50+
continue-on-error: true
51+
run: .github/assert-git-changes.py
52+
env:
53+
TARGET_DIR: lib/abi/
54+
55+
- name: Create Pull Request
56+
if: steps.changes.outcome != 'success'
57+
uses: lidofinance/create-pull-request@v4
58+
with:
59+
branch: fix-abi-${{ github.ref_name }}
60+
delete-branch: true
61+
commit-message: "fix: Make ABIs up to date"
62+
title: "Fix ABI ${{ github.ref_name }}"
63+
body: "This PR is generated automatically. Merge it to apply fixes to the /lib/abi/"

.github/workflows/linters.yml

+51-5
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,10 @@ jobs:
1414
uses: actions/setup-node@v1
1515
with:
1616
node-version: 12.x
17-
17+
1818
- name: Get yarn cache directory path
1919
id: yarn-cache-dir-path
20-
run: echo "::set-output name=dir::$(yarn cache dir)"
20+
run: echo "::set-output name=dir::$(yarn config get cacheFolder)"
2121

2222
- name: Cache yarn cache
2323
id: cache-yarn-cache
@@ -42,8 +42,8 @@ jobs:
4242
steps.cache-node-modules.outputs.cache-hit != 'true'
4343
4444
- name: Run Solidity tests
45-
run: yarn test:unit
46-
45+
run: yarn test:unit
46+
4747
- name: Run Solidity linters
4848
run: yarn lint:sol:solhint
4949

@@ -62,7 +62,7 @@ jobs:
6262
uses: actions/setup-node@v1
6363
with:
6464
node-version: 12.x
65-
65+
6666
- name: Get yarn cache directory path
6767
id: yarn-cache-dir-path
6868
run: echo "::set-output name=dir::$(yarn cache dir)"
@@ -92,3 +92,49 @@ jobs:
9292
- name: Run Solidity test coverage
9393
run: yarn test:coverage
9494
continue-on-error: false
95+
96+
abi-lint:
97+
name: ABI actuality linter
98+
runs-on: ubuntu-latest
99+
100+
steps:
101+
- uses: actions/checkout@v2
102+
103+
- name: Setup node.js version
104+
uses: actions/setup-node@v1
105+
with:
106+
node-version: 12.x
107+
108+
- name: Get yarn cache directory path
109+
id: yarn-cache-dir-path
110+
run: echo "::set-output name=dir::$(yarn config get cacheFolder)"
111+
112+
- name: Cache yarn cache
113+
id: cache-yarn-cache
114+
uses: actions/cache@v2
115+
with:
116+
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
117+
key: yarn-${{ hashFiles('**/yarn.lock') }}
118+
restore-keys: yarn-${{ hashFiles('**/yarn.lock') }}
119+
120+
- name: Cache node_modules
121+
id: cache-node-modules
122+
uses: actions/cache@v2
123+
with:
124+
path: '**/node_modules'
125+
key: node_modules-${{ hashFiles('**/yarn.lock') }}
126+
restore-keys: node_modules-${{ hashFiles('**/yarn.lock') }}
127+
128+
- name: Install modules
129+
run: yarn
130+
if: |
131+
steps.cache-yarn-cache.outputs.cache-hit != 'true' ||
132+
steps.cache-node-modules.outputs.cache-hit != 'true'
133+
134+
- name: Compile code and extract ABI
135+
run: yarn compile
136+
137+
- name: Check for ABI changes
138+
run: .github/assert-git-changes.py
139+
env:
140+
TARGET_DIR: lib/abi/

0 commit comments

Comments
 (0)