Skip to content

Commit 763e6da

Browse files
committed
Initial commit
1 parent 7e925eb commit 763e6da

20 files changed

+3013
-38
lines changed

.github/workflows/deploy.yml

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
name: Deploy Sleuth [Mainnet]
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
deployer_address:
7+
description: WalletConnect address to deploy from
8+
required: true
9+
10+
env:
11+
FOUNDRY_PROFILE: ci
12+
13+
jobs:
14+
check:
15+
strategy:
16+
fail-fast: true
17+
18+
name: Deploy Sleuth [Mainnet]
19+
runs-on: ubuntu-latest
20+
steps:
21+
- name: Start Seacrest
22+
uses: hayesgm/seacrest@v1
23+
with:
24+
ethereum_url: "${{ secrets.ETH_MAINNET_URL }}"
25+
26+
- uses: actions/checkout@v3
27+
with:
28+
submodules: recursive
29+
30+
- name: Install Foundry
31+
uses: foundry-rs/foundry-toolchain@v1
32+
with:
33+
version: nightly
34+
35+
- name: Run Forge build
36+
run: |
37+
forge --version
38+
forge build --sizes
39+
40+
- name: Forge Deploy Sleuth [Mainnet]
41+
run: script/mainnet/deploy.sh
42+
env:
43+
ETHERSCAN_API_KEY: "${{ secrets.ETHERSCAN_API_KEY }}"
44+
ETH_FROM: "${{ inputs.deployer_address }}"
45+
RPC_URL: "http://localhost:8585"

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,6 @@ out/
99

1010
# Dotenv file
1111
.env
12+
dist
13+
node_modules
14+
yarn-error.log

README.md

+73
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# Sleuth
2+
3+
<img src="https://github.com/compound-finance/sleuth/raw/main/logo.png" width="100">
4+
5+
----
6+
7+
Sleuth is an easy way to pull data from an EVM-compatible blockchain, allowing for complex queries, similar to an ethers-multicall. Sleuth works by deploying a smart contract and then invoking it in an `eth_call`. This allows you to use complex logic to pull data from many contracts or other items such as `eth_chainId` or `eth_blockNumber`, which you can use for data analysis or in your Web3 front-end. For example:
8+
9+
**MyQuery.sol** [Note: this is not deployed, and is never deployed]
10+
```sol
11+
// SPDX-License-Identifier: UNLICENSED
12+
pragma solidity ^0.8.16;
13+
14+
contract BlockNumber {
15+
function query() external view returns (uint256) {
16+
return block.number;
17+
}
18+
}
19+
```
20+
21+
**MyView.ts**
22+
```ts
23+
import { Sleuth } from '@compound-finance/sleuth';
24+
25+
let sleuth = new Sleuth(provider);
26+
27+
let blockNumber = await sleuth.query(fs.readFileSync('./MyQuery.sol', 'utf8'));
28+
```
29+
30+
## Getting Started
31+
32+
Install Sleuth:
33+
34+
```
35+
yarn add @compound-finance/sleuth
36+
37+
# npm install --save @compound-finance/sleuth
38+
```
39+
40+
Next, simply build a Solidity file and build Sleuth, as above, to execute the query. E.g.
41+
42+
```ts
43+
import { Sleuth } from '@compound-finance/sleuth';
44+
45+
let sleuth = new Sleuth(provider);
46+
47+
let [name, age] = await sleuth.query(`
48+
// SPDX-License-Identifier: UNLICENSED
49+
pragma solidity ^0.8.16;
50+
51+
contract SimpleQuery {
52+
function query() external pure returns (uint256, string memory) {
53+
return (55, "Bob Jones");
54+
}
55+
}
56+
`);
57+
```
58+
59+
## Future Considerations
60+
61+
Instead of having users build solidity files, it might be nice to build a proper query language. This could be SQL-like or ORM-style or anything that compiles to say Yul (the intermediate representation used by Solidity). We could then abstract the interface to something interesting, such as:
62+
63+
```ts
64+
await sleuth.query("SELECT comet.name FROM comet(0xc3...) WHERE comet.id = 5");
65+
```
66+
67+
There's so much we could do here and it sounds really fun!
68+
69+
## License
70+
71+
MIT
72+
73+
Copyright 2022, Compound Labs, Inc. Geoffrey Hayes.

cli/sleuth.ts

+80
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
const solc = require('solc');
2+
import { Provider } from '@ethersproject/providers';
3+
import { Contract } from '@ethersproject/contracts';
4+
import { AbiCoder } from '@ethersproject/abi';
5+
import { keccak256 } from '@ethersproject/keccak256';
6+
import { getContractAddress } from '@ethersproject/address';
7+
8+
interface Opts {
9+
network?: string,
10+
version?: number
11+
};
12+
13+
const defaultOpts = {
14+
network: 'mainnet',
15+
version: 1
16+
};
17+
18+
const sleuthDeployer = process.env['SLEUTH_ADDRESS'] ?? '0x84C3e20985d9E7aEc46F80d2EB52b731D8CC40F8';
19+
20+
export class Sleuth {
21+
provider: Provider;
22+
network: string;
23+
version: number;
24+
sleuthAddr: string;
25+
26+
constructor(provider: Provider, opts: Opts = {}) {
27+
this.provider = provider;
28+
this.network = opts.network ?? defaultOpts.network;
29+
this.version = opts.version ?? defaultOpts.version;
30+
this.sleuthAddr = getContractAddress({ from: sleuthDeployer, nonce: this.version - 1 });
31+
console.log('Sleuth address', this.sleuthAddr);
32+
}
33+
34+
async query(q: string) {
35+
const input = {
36+
language: 'Solidity',
37+
sources: {
38+
'query.sol': {
39+
content: q
40+
}
41+
},
42+
settings: {
43+
outputSelection: {
44+
'*': {
45+
'*': ['*']
46+
}
47+
}
48+
}
49+
};
50+
51+
let result = JSON.parse(solc.compile(JSON.stringify(input)));
52+
if (result.errors) {
53+
throw new Error("Compilation Error: " + JSON.stringify(result.errors));
54+
}
55+
let contract = result.contracts['query.sol'];
56+
if (!contract) {
57+
throw new Error(`Missing query.sol compiled contract in ${JSON.stringify(Object.keys(result.contracts))}`);
58+
}
59+
let c = Object.values(contract)[0] as any;
60+
if (!c) {
61+
throw new Error(`Query does not contain any contract definitions`);
62+
} else if (Object.keys(contract).length > 1) {
63+
console.warn(`Query contains multiple contracts, using ${Object.keys(contract)[0]}`);
64+
}
65+
let b = c.evm.bytecode.object;
66+
let abi = c.abi;
67+
let queryAbi = abi.find(({type, name}: any) => type === 'function' && name === 'query');
68+
if (!queryAbi) {
69+
throw new Error(`Query must include function \`query()\``);
70+
}
71+
let sleuthCtx = new Contract(this.sleuthAddr, ['function query(bytes) public view returns (bytes)'], this.provider);
72+
let queryResult = await sleuthCtx.query('0x' + b);
73+
let res = new AbiCoder().decode(queryAbi.outputs, queryResult);
74+
if (res.length === 1) {
75+
return res[0]
76+
} else {
77+
return res;
78+
}
79+
}
80+
}

cli/test/client.test.ts

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { Sleuth } from '../sleuth';
2+
import { Provider, JsonRpcProvider } from '@ethersproject/providers';
3+
import * as fs from 'fs/promises';
4+
import * as path from 'path';
5+
6+
describe('testing sleuthing', () => {
7+
let provider: Provider;
8+
9+
beforeAll(() => {
10+
provider = new JsonRpcProvider('http://127.0.0.1:8599');
11+
});
12+
13+
test('should return the block number', async () => {
14+
let sleuth = new Sleuth(provider);
15+
let res = await sleuth.query(await fs.readFile(path.join(__dirname, '../../src/examples/BlockNumber.sol'), 'utf8'));
16+
expect(res.toNumber()).toBe(1);
17+
});
18+
19+
test('should return the pair', async () => {
20+
let sleuth = new Sleuth(provider);
21+
let res = await sleuth.query(await fs.readFile(path.join(__dirname, '../../src/examples/Pair.sol'), 'utf8'));
22+
expect(res[0].toNumber()).toBe(55);
23+
expect(res[1]).toEqual("hello");
24+
});
25+
});

foundry.toml

+1
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,6 @@
22
src = 'src'
33
out = 'out'
44
libs = ['lib']
5+
fs_permissions = [{ access = "read", path = "./out"}]
56

67
# See more config options https://github.com/foundry-rs/foundry/tree/master/config

jest.config.js

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
module.exports = {
2+
transform: {'^.+\\.ts?$': 'ts-jest'},
3+
testEnvironment: 'node',
4+
testRegex: 'cli/test/.*\\.(test|spec)?\\.(ts|tsx)$',
5+
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node']
6+
};

logo.png

1.24 MB
Loading

package.json

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"name": "sleuth",
3+
"version": "1.0.0",
4+
"main": "dist/index.js",
5+
"repository": "https://github.com/compound-finance/sleuth",
6+
"author": "Geoffrey Hayes <hayesgm@gmail.com>",
7+
"license": "MIT",
8+
"scripts": {
9+
"test": "jest"
10+
},
11+
"devDependencies": {
12+
"@types/jest": "^29.2.4",
13+
"jest": "^29.3.1",
14+
"ts-jest": "^29.0.3",
15+
"typescript": "^4.9.4"
16+
},
17+
"dependencies": {
18+
"@ethersproject/contracts": "^5.7.0",
19+
"@ethersproject/providers": "^5.7.2",
20+
"solc": "^0.8.17"
21+
}
22+
}

script/Counter.s.sol script/Sleuth.s.sol

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ pragma solidity ^0.8.13;
33

44
import "forge-std/Script.sol";
55

6-
contract CounterScript is Script {
6+
contract SleuthScript is Script {
77
function setUp() public {}
88

99
function run() public {

script/mainnet/deploy.sh

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
#!/bin/bash
2+
3+
set -exo pipefail
4+
5+
if [ -n "$ETHEREUM_PK" ]; then
6+
wallet_args="--private-key $ETHEREUM_PK"
7+
else
8+
wallet_args="--unlocked"
9+
fi
10+
11+
if [ -n "$RPC_URL" ]; then
12+
rpc_args="--rpc-url $RPC_URL"
13+
else
14+
rpc_args=""
15+
fi
16+
17+
if [ -n "$ETHERSCAN_API_KEY" ]; then
18+
etherscan_args="--verify --etherscan-api-key $ETHERSCAN_API_KEY"
19+
else
20+
etherscan_args=""
21+
fi
22+
23+
forge create \
24+
$rpc_args \
25+
$etherscan_args \
26+
$wallet_args \
27+
$@ \
28+
src/Sleuth.sol:Sleuth

script/test.sh

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
#!/bin/bash
2+
3+
set -exo pipefail
4+
5+
anvil --port 8599 &
6+
anvil_pid="$!"
7+
sleep 3
8+
9+
if kill -0 "$anvil_pid"; then
10+
echo "anvil running"
11+
else
12+
echo "anvil failed"
13+
wait "$anvil_pid"
14+
fi
15+
16+
while ! nc -z localhost 8599; do
17+
sleep 3
18+
done
19+
20+
function cleanup {
21+
kill "$anvil_pid"
22+
}
23+
24+
trap cleanup EXIT
25+
26+
forge build
27+
forge create --rpc-url http://localhost:8599 --private-key 0x2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6 src/Sleuth.sol:Sleuth
28+
29+
SLEUTH_DEPLOYER=0xa0ee7a142d267c1f36714e4a8f75612f20a79720 yarn test

src/Counter.sol

-14
This file was deleted.

src/Sleuth.sol

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// SPDX-License-Identifier: UNLICENSED
2+
pragma solidity ^0.8.16;
3+
4+
contract Sleuth {
5+
6+
function query(bytes calldata q) external returns (bytes memory) {
7+
return queryInternal(q, abi.encodeWithSignature("query()"));
8+
}
9+
10+
function query(bytes calldata q, bytes memory c) external returns (bytes memory) {
11+
return queryInternal(q, c);
12+
}
13+
14+
function queryInternal(bytes memory q, bytes memory c) internal returns (bytes memory) {
15+
assembly {
16+
let queryLen := mload(q)
17+
let queryStart := add(q, 0x20)
18+
let deployment := create(0, queryStart, queryLen)
19+
let callLen := mload(c)
20+
let callStart := add(c, 0x20)
21+
pop(call(gas(), deployment, 0, callStart, callLen, 0xc0, 0))
22+
returndatacopy(0xc0, 0, returndatasize())
23+
mstore(0x80, 0x20)
24+
mstore(0xa0, returndatasize())
25+
let sz := add(returndatasize(), 0x40)
26+
return(0x80, sz)
27+
}
28+
}
29+
}

src/examples/BlockNumber.sol

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
// SPDX-License-Identifier: UNLICENSED
2+
pragma solidity ^0.8.16;
3+
4+
contract BlockNumber {
5+
function query() external view returns (uint256) {
6+
return block.number;
7+
}
8+
}

0 commit comments

Comments
 (0)