Skip to content

Commit 6958664

Browse files
committed
foundry integration
1 parent 3d4dc0d commit 6958664

File tree

9 files changed

+184
-159
lines changed

9 files changed

+184
-159
lines changed

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ Example contract with `init` function:
4646
```rust
4747
#[public]
4848
impl TestCounter {
49-
pub fn init(&mut self, counter: Address) {
49+
pub fn setUp(&mut self, counter: Address) {
5050
self.counter.set(counter);
5151
}
5252
// ...
@@ -65,7 +65,7 @@ Example `skribe.json` file for the contract above:
6565

6666
#### Test functions
6767

68-
Test functions must start with the `test_` prefix and return either `bool` or `()`. A panic or a `false` result is
68+
Test functions must start with the `test_` prefix and return `()`. A panic is
6969
considered a test failure. Skribe automatically discovers these test functions and runs them with randomized input
7070
values as part of the fuzzing process.
7171

@@ -76,11 +76,11 @@ Example test function:
7676
impl TestCounter {
7777
// ...
7878

79-
pub fn test_call_set_get_number(&mut self, x: U256) -> bool {
79+
pub fn test_call_set_get_number(&mut self, x: U256) {
8080
let counter = ICounter::new(self.counter.get());
8181
counter.set_number(Call::new_in(self), x).unwrap();
8282

83-
counter.number(self).unwrap() == x
83+
assert_eq!(counter.number(self).unwrap(), x)
8484
}
8585
}
8686
```

src/skribe/__main__.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,11 @@ def _exec_build(dir_path: Path | None) -> None:
2929
dir_path = Path.cwd() if dir_path is None else dir_path
3030

3131
skribe = Skribe(concrete_definition)
32-
skribe.build_stylus_contract(contract_dir=dir_path)
32+
33+
if (dir_path / 'foundry.toml').exists():
34+
skribe.build_foundry_contract(contract_dir=dir_path)
35+
else:
36+
skribe.build_stylus_contract(contract_dir=dir_path)
3337

3438
exit(0)
3539

src/skribe/contract.py

Lines changed: 91 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -8,132 +8,119 @@
88

99
from eth_abi.tools._strategies import get_abi_strategy
1010
from hypothesis import strategies
11-
from pyk.utils import run_process
11+
from kontrol.solc_to_k import Contract as EVMContract
12+
from kontrol.solc_to_k import contract_name_with_path, method_sig_from_abi
13+
from pyk.kast.inner import KSort
14+
from pyk.utils import run_process, single
1215

13-
from .simulation import call_data
16+
from skribe.simulation import call_data
1417

1518
if TYPE_CHECKING:
1619

1720
from hypothesis.strategies import SearchStrategy
1821

1922

20-
@dataclass(frozen=True)
21-
class ContractBinding:
22-
"""Represents one of the function bindings for a Stylus contract."""
23+
type Method = EVMContract.Method
2324

24-
name: str
25-
inputs: tuple[str, ...]
26-
outputs: tuple[str, ...]
2725

28-
@staticmethod
29-
def from_dict(d: dict[str, Any]) -> ContractBinding:
30-
name = d['name']
31-
inputs = tuple(inp['type'] for inp in d['inputs'])
32-
outputs = tuple(out['type'] for out in d['outputs'])
33-
return ContractBinding(name, inputs, outputs)
26+
@dataclass
27+
class StylusContract:
28+
contract_path: Path
29+
_cargo_bin: Path
3430

35-
@cached_property
36-
def strategy(self) -> SearchStrategy[bytes]:
37-
input_strategies = (get_abi_strategy(arg) for arg in self.inputs)
38-
tuple_strategy = strategies.tuples(*input_strategies)
39-
40-
encoder = partial(call_data, self.name, self.inputs)
41-
return tuple_strategy.map(encoder)
31+
def __init__(self, cargo_bin: Path, contract_dir: Path):
32+
self.contract_path = contract_dir.resolve()
33+
self._cargo_bin = cargo_bin
4234

4335
@cached_property
44-
def is_test(self) -> bool:
45-
return self.name.startswith('test')
36+
def name_with_path(self) -> str:
37+
return contract_name_with_path(str(self.contract_path), self._name)
4638

4739
@cached_property
48-
def arity(self) -> int:
49-
return len(self.inputs)
50-
40+
def manifest_path(self) -> Path:
41+
return self.contract_path / 'Cargo.toml'
5142

52-
@dataclass(frozen=True)
53-
class ContractMetadata:
54-
manifest_path: Path
55-
name: str
56-
bindings: tuple[ContractBinding, ...]
57-
target_dir: Path
43+
@cached_property
44+
def manifest(self) -> dict[str, Any]:
45+
return json.loads(
46+
run_process(
47+
[
48+
str(self._cargo_bin),
49+
'metadata',
50+
'--no-deps',
51+
'--manifest-path',
52+
str(self.manifest_path),
53+
'--format-version',
54+
'1',
55+
],
56+
check=True,
57+
).stdout
58+
)
5859

5960
@cached_property
60-
def wasm_target_dir(self) -> Path:
61-
return (self.target_dir / 'wasm32-unknown-unknown' / 'release').resolve()
61+
def contract_package(self) -> dict[str, Any]:
62+
return single(p for p in self.manifest['packages'] if Path(p['manifest_path']) == self.manifest_path)
6263

6364
@cached_property
64-
def wasm_path(self) -> Path:
65-
wasm_file_name = self.name.replace('-', '_') + '.wasm'
66-
wasm_path = self.wasm_target_dir / wasm_file_name
67-
return wasm_path.resolve()
65+
def _name(self) -> str:
66+
return self.contract_package['name']
6867

6968
@cached_property
70-
def init_func(self) -> ContractBinding | None:
71-
for b in self.bindings:
72-
if b.name == 'init':
73-
return b
74-
return None
69+
def abi(self) -> list[dict[str, Any]]:
70+
proc_res = run_process(
71+
[str(self._cargo_bin), 'stylus', 'export-abi', '--json'],
72+
cwd=self.contract_path,
73+
check=True,
74+
)
75+
json_output = proc_res.stdout.split('\n', 3)[3] # remove the headers
76+
return json.loads(json_output)
7577

7678
@cached_property
77-
def has_init(self) -> bool:
78-
return self.init_func is not None
79+
def methods(self) -> tuple[Method, ...]:
80+
return tuple(
81+
EVMContract.Method(
82+
msig=method_sig_from_abi(method_abi, True),
83+
id=0,
84+
abi=method_abi,
85+
ast=None,
86+
contract_name_with_path='',
87+
contract_digest='',
88+
contract_storage_digest='',
89+
sort=KSort(f'{EVMContract.escaped(self.name_with_path, "S2K")}Method'),
90+
devdoc=None,
91+
function_calls=None,
92+
)
93+
for method_abi in self.abi
94+
if method_abi['type'] == 'function'
95+
)
7996

8097
@cached_property
81-
def test_functions(self) -> tuple[ContractBinding, ...]:
82-
return tuple(b for b in self.bindings if b.is_test)
83-
84-
def typecheck(self) -> None:
85-
for b in self.bindings:
86-
if b.name == 'init':
87-
no_output = not b.outputs
88-
only_address = all(i == 'address' for i in b.inputs)
89-
if no_output and only_address:
90-
continue
91-
if not b.is_test:
92-
continue
93-
if not b.outputs:
94-
continue
95-
if b.outputs == ('bool',):
96-
continue
97-
98-
raise TypeError(f'Invalid type: {b.name}{b.inputs} -> {b.outputs}')
99-
100-
101-
def read_contract_bindings(cargo_bin: Path, contract_dir: Path) -> tuple[ContractBinding, ...]:
102-
"""Reads a stylus wasm contract, and returns a list of the function bindings for it."""
103-
proc_res = run_process(
104-
[str(cargo_bin), 'stylus', 'export-abi', '--json'],
105-
cwd=contract_dir,
106-
check=True,
107-
)
108-
json_output = proc_res.stdout.split('\n', 3)[3] # remove the headers
109-
bindings_list = json.loads(json_output)
110-
111-
return tuple(
112-
ContractBinding.from_dict(binding_dict) for binding_dict in bindings_list if binding_dict['type'] == 'function'
113-
)
114-
115-
116-
def read_contract_metadata(cargo_bin: Path, contract_dir: Path) -> ContractMetadata:
117-
manifest_path = (contract_dir / 'Cargo.toml').resolve()
118-
proc_res = run_process(
119-
[str(cargo_bin), 'metadata', '--no-deps', '--manifest-path', str(manifest_path), '--format-version', '1'],
120-
check=True,
121-
)
122-
manifest = json.loads(proc_res.stdout)
123-
124-
# filter out other packages in the workspace and get the one that matches the contract
125-
contract_package = [p for p in manifest['packages'] if Path(p['manifest_path']) == manifest_path][0]
126-
name = contract_package['name']
127-
target_dir = Path(manifest['target_directory']).resolve()
128-
129-
bindings = read_contract_bindings(cargo_bin, contract_dir)
130-
131-
res = ContractMetadata(
132-
manifest_path=manifest_path,
133-
name=name,
134-
bindings=bindings,
135-
target_dir=target_dir,
136-
)
137-
res.typecheck()
138-
139-
return res
98+
def deployed_bytecode(self) -> bytes:
99+
wasm_file_name = self._name.replace('-', '_') + '.wasm'
100+
wasm_path = Path(self.manifest['target_directory']) / 'wasm32-unknown-unknown' / 'release' / wasm_file_name
101+
return wasm_path.read_bytes()
102+
103+
104+
type ArbitrumContract = EVMContract | StylusContract
105+
106+
107+
def setup_method(c: ArbitrumContract) -> Method | None:
108+
for m in c.methods:
109+
if m.name == 'setUp':
110+
return m
111+
return None
112+
113+
114+
def is_foundry_test(ctr: EVMContract) -> bool:
115+
if ctr.is_test_contract:
116+
for m in ctr.methods:
117+
if m.is_test:
118+
return True
119+
return False
120+
121+
122+
def argument_strategy(m: Method) -> SearchStrategy[bytes]:
123+
input_strategies = (get_abi_strategy(arg) for arg in m.arg_types)
124+
tuple_strategy = strategies.tuples(*input_strategies)
125+
encoder = partial(call_data, m.name, m.arg_types)
126+
return tuple_strategy.map(encoder)

src/skribe/kdist/plugin.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from typing import TYPE_CHECKING
66

77
from kevm_pyk.kompile import KompileTarget, kevm_kompile
8+
from kontrol.kdist.utils import KSRC_DIR as FOUNDRY_KSRC_DIR
89
from pyk.kbuild.utils import k_version
910
from pyk.kdist.api import Target
1011

@@ -72,7 +73,7 @@ def context(self) -> dict[str, str]:
7273
'main_file': src_dir / 'stylus-semantics/skribe.md',
7374
'main_module': 'SKRIBE',
7475
'syntax_module': 'SKRIBE-SYNTAX',
75-
'includes': [src_dir],
76+
'includes': [src_dir, FOUNDRY_KSRC_DIR],
7677
},
7778
),
7879
}

src/skribe/progress.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,13 @@
99

1010
from rich.progress import TaskID
1111

12-
from .contract import ContractBinding
12+
from .contract import Method
1313

1414

1515
class FuzzProgress(Progress):
1616
fuzz_tasks: list[FuzzTask]
1717

18-
def __init__(self, bindings: Iterable[ContractBinding], max_examples: int):
18+
def __init__(self, bindings: Iterable[Method], max_examples: int):
1919
super().__init__(
2020
TextColumn('[progress.description]{task.description}'),
2121
BarColumn(),
@@ -33,11 +33,11 @@ def __init__(self, bindings: Iterable[ContractBinding], max_examples: int):
3333

3434

3535
class FuzzTask:
36-
binding: ContractBinding
36+
binding: Method
3737
task_id: TaskID
3838
progress: FuzzProgress
3939

40-
def __init__(self, binding: ContractBinding, task_id: TaskID, progress: FuzzProgress):
40+
def __init__(self, binding: Method, task_id: TaskID, progress: FuzzProgress):
4141
self.binding = binding
4242
self.task_id = task_id
4343
self.progress = progress

0 commit comments

Comments
 (0)