Skip to content

Commit

Permalink
docs: add sevm precompile detection analysis tool (#826)
Browse files Browse the repository at this point in the history
* 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
acuarica authored Jun 27, 2024
1 parent 2251206 commit 7af9077
Show file tree
Hide file tree
Showing 6 changed files with 1,251 additions and 0 deletions.
3 changes: 3 additions & 0 deletions tools/custom/sevm/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/node_modules/
/.testnet/
/testnet.sqlite
54 changes: 54 additions & 0 deletions tools/custom/sevm/README.md
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>
```
89 changes: 89 additions & 0 deletions tools/custom/sevm/analyze.js
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);
69 changes: 69 additions & 0 deletions tools/custom/sevm/fetch.js
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);
13 changes: 13 additions & 0 deletions tools/custom/sevm/package.json
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"
}
}
Loading

0 comments on commit 7af9077

Please sign in to comment.