-
Notifications
You must be signed in to change notification settings - Fork 55
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
docs: add
sevm
precompile detection analysis tool (#826)
* Add analysis and support to fetch contracts Signed-off-by: Luis Mastrangelo <[email protected]> * Improve analysis output Signed-off-by: Luis Mastrangelo <[email protected]> * Add `README` Signed-off-by: Luis Mastrangelo <[email protected]> * Improve `README` Signed-off-by: Luis Mastrangelo <[email protected]> * Change license after Logan suggestion Signed-off-by: Luis Mastrangelo <[email protected]> * Implement Logan's suggestions Signed-off-by: Luis Mastrangelo <[email protected]> --------- Signed-off-by: Luis Mastrangelo <[email protected]>
- Loading branch information
Showing
6 changed files
with
1,251 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
/node_modules/ | ||
/.testnet/ | ||
/testnet.sqlite |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
# `sevm` Precompiles Detection Analysis | ||
|
||
`sevm` is a Symbolic Ethereum Virtual Machine (EVM) bytecode interpreter, parser and decompiler, along with several other utils for programmatically extracting information from EVM bytecode. | ||
In particular, it provides [Hooks](https://github.com/acuarica/evm?tab=readme-ov-file#advanced-usage) that allows the user to intercept when each bytecode is symbolically executed. | ||
|
||
## Scripts | ||
|
||
### [`fetch.js`](./fetch.js) | ||
|
||
To fetch contracts simply execute | ||
|
||
```sh | ||
./fetch.js | ||
``` | ||
|
||
It uses the [Mirror Node API](https://testnet.mirrornode.hedera.com/api/v1/docs/) to fetch contracts from the `testnet` network. | ||
|
||
The fetched contracts are stored in the `.testnet` folder. | ||
Please note that these contracts are not stored by address. | ||
Instead, they are stored using the bytecode `keccak256` hash as file name. | ||
This is to avoid storing and analyzing duplicated contracts. | ||
|
||
It creates a SQLite database `testnet.sqlite` to keep track of which contract addresses were fetched. | ||
This is two-fold. | ||
On the one hand, it is used to avoid re-downloading already downloaded contracts. | ||
On the other hand, it maps contract addresses to bytecode in the filesystem. | ||
|
||
### [`analyze.js`](./analyze.js) | ||
|
||
To run the analysis execute | ||
|
||
```sh | ||
./analyze.js | ||
``` | ||
|
||
This script traverses the fetched contracts in the `.testnet` folder and tries to find the calls (`CALL`, `STATICCALL` and `DELEGATECALL`) to addresses whose value can be determine at compile-time. | ||
For this step, it uses `sevm` to parse and symbolically execute the contract bytecode. | ||
|
||
The output should look like this (your mileage may vary) | ||
|
||
```console | ||
[..] | ||
0x167 .testnet/4b/0x4b601e[..]4b6507.bytecode call(gasleft(),0x167,local39,local41 + local8,memory[local41],local39,local39) | ||
0x167 .testnet/4b/0x4b601e[..]4b6507.bytecode call(gasleft(),0x167,local39,local41 + local8,memory[local41],local39,local39) | ||
0x167 .testnet/93/0x937dc4[..]2f7f8b.bytecode call(gasleft(),0x167,local46,local41 + 0x20,memory[local41],local46,local46) | ||
0x167 .testnet/93/0x937dc4[..]2f7f8b.bytecode call(gasleft(),0x167,local47,local42 + 0x20,memory[local42],local47,local47) | ||
[..] | ||
``` | ||
|
||
The output format is the following | ||
|
||
```console | ||
<contract address> <precompile address> <bytecode path> <call/staticcall/deletegatecall where precompile address is used> | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
#!/usr/bin/env node | ||
|
||
import c from 'chalk'; | ||
import * as fs from 'fs'; | ||
import { Contract, Shanghai, sol } from 'sevm'; | ||
import sqlite3 from 'sqlite3'; | ||
import { open } from 'sqlite'; | ||
|
||
const info = (message, ...optionalParams) => console.info(c.dim('[info]'), message, ...optionalParams); | ||
|
||
const name = 'testnet'; | ||
|
||
/** | ||
* @param {sqlite3.Database} db | ||
* @param {string} code | ||
* @param {string} hash | ||
* @param {string} path | ||
*/ | ||
async function analyze(db, code, hash, path) { | ||
const row = await db.get('SELECT address FROM contracts WHERE hash = ?', hash); | ||
const contractAddress = row?.address; | ||
|
||
try { | ||
new Contract(code, new class extends Shanghai { | ||
STATICCALL = (state) => { | ||
super.STATICCALL(state); | ||
const call = state.stack.top; | ||
const address = call.address.eval(); | ||
if (address.tag === 'Val') { | ||
console.log(contractAddress, '0x' + address.val.toString(16), path, sol`${call.eval()}`) | ||
} | ||
}; | ||
|
||
CALL = (state) => { | ||
super.CALL(state); | ||
const call = state.stack.top; | ||
const address = call.address.eval(); | ||
if (address.tag === 'Val') { | ||
console.log(contractAddress, '0x' + address.val.toString(16), path, sol`${call.eval()}`) | ||
} | ||
}; | ||
|
||
DELEGATECALL = (state) => { | ||
super.DELEGATECALL(state); | ||
const call = state.stack.top; | ||
const address = call.address.eval(); | ||
if (address.tag === 'Val') { | ||
console.log(contractAddress, '0x' + address.val.toString(16), path, sol`${call.eval()}`) | ||
} | ||
}; | ||
}()); | ||
} catch (err) { | ||
console.info(path, err); | ||
} | ||
} | ||
|
||
const shortened = hash => hash.slice(0, 8) + '[..]' + hash.slice(-6); | ||
|
||
async function main() { | ||
const dbname = `${name}.sqlite`; | ||
info('Opening db', c.magenta(dbname)); | ||
|
||
const db = await open({ | ||
filename: dbname, | ||
driver: sqlite3.Database | ||
}); | ||
|
||
let prefixes = process.argv.slice(2); | ||
prefixes = prefixes.length === 0 ? fs.readdirSync(`.${name}`) : prefixes; | ||
|
||
for (const prefix of prefixes) { | ||
// process.stdout.write(`${c.dim(prefix)} `); | ||
for (const file of fs.readdirSync(`.${name}/${prefix}`)) { | ||
const path = `.${name}/${prefix}/${file}`; | ||
// console.info(`Running ${c.cyan('sevm')} analysis on ${c.magenta(file.slice(0, 8) + '..' + file.slice(-9 - 6))}`); | ||
const code = fs.readFileSync(path, 'utf8'); | ||
|
||
if (code === '0x') { | ||
continue; | ||
} | ||
|
||
const [hash, ext] = file.split('.'); | ||
const shortenedPath = `.${name}/${prefix}/${shortened(hash)}.${ext}` | ||
await analyze(db, code, hash, shortenedPath); | ||
} | ||
} | ||
} | ||
|
||
main().catch(console.error); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
#!/usr/bin/env node | ||
|
||
import c from 'chalk'; | ||
import sqlite3 from 'sqlite3'; | ||
import { open } from 'sqlite'; | ||
import * as path from 'path'; | ||
import { keccak256 } from 'ethers'; | ||
import { mkdir, writeFile } from 'fs/promises'; | ||
|
||
const name = 'testnet'; | ||
|
||
const info = (message, ...optionalParams) => console.info(c.dim('[info]'), message, ...optionalParams); | ||
|
||
/** | ||
* | ||
* @param {sqlite3.Database} db | ||
* @param {string} contract_id | ||
* @param {string} address | ||
*/ | ||
async function fetchCode(db, contract_id, address) { | ||
const { bytecode: content } = await (await fetch(`https://testnet.mirrornode.hedera.com/api/v1/contracts/${address}`)).json(); | ||
|
||
const hash = keccak256(content); | ||
|
||
await Promise.all([ | ||
writeFile(path.join(`.${name}`, hash.slice(2, 4), hash + '.bytecode'), content, 'utf8'), | ||
db.run('INSERT INTO contracts (address, contract_id, hash, size) VALUES ($address, $contract_id, $hash, $size)', { | ||
$address: address, | ||
$contract_id: contract_id, | ||
$hash: hash, | ||
$size: content.length, | ||
}) | ||
]); | ||
} | ||
|
||
async function main() { | ||
const dbname = `${name}.sqlite`; | ||
|
||
info('Opening db', c.magenta(dbname)); | ||
const db = await open({ | ||
filename: dbname, | ||
driver: sqlite3.Database | ||
}); | ||
|
||
await db.exec('CREATE TABLE IF NOT EXISTS contracts (address TEXT PRIMARY KEY ON CONFLICT REPLACE, contract_id TEXT NOT NULL, hash TEXT NOT NULL, size INTEGER NOT NULL) STRICT'); | ||
|
||
info(`Creating directory prefixes under ${c.magenta('.' + name)}`); | ||
for (let i = 0; i < 256; i++) { | ||
const prefix = i.toString(16).padStart(2, '0'); | ||
await mkdir(path.join(`.${name}`, prefix), { recursive: true }); | ||
} | ||
|
||
const row = await db.get('SELECT MIN(contract_id) AS contract_id FROM contracts'); | ||
const params = row.contract_id ? `&contract.id=lt:${row.contract_id}` : ''; | ||
|
||
let next = `https://${name}.mirrornode.hedera.com/api/v1/contracts?limit=100&order=desc${params}`; | ||
for (let i = 0; i < 20; i++) { | ||
info(`Using URL \`${next}\` to fetch the next contracts`) | ||
let { contracts, links } = await (await fetch(next)).json(); | ||
for (const contract of contracts) { | ||
info(`Fetching code ${contract.contract_id}:${contract.evm_address}`) | ||
await fetchCode(db, contract.contract_id, contract.evm_address); | ||
} | ||
|
||
next = `https://${name}.mirrornode.hedera.com${links.next}`; | ||
} | ||
} | ||
|
||
main().catch(console.error); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
{ | ||
"name": "hedera-sevm-precompile-detection-analysis", | ||
"version": "0.0.1", | ||
"type": "module", | ||
"license": "Apache-2.0", | ||
"devDependencies": { | ||
"chalk": "^5.3.0", | ||
"ethers": "^6.3.0", | ||
"sevm": "^0.6.17", | ||
"sqlite": "^5.0.1", | ||
"sqlite3": "^5.1.6" | ||
} | ||
} |
Oops, something went wrong.