precompile_utils/testing/
solidity.rs

1// This file is part of Frontier.
2
3// Copyright (c) Moonsong Labs.
4// Copyright (C) Parity Technologies (UK) Ltd.
5// SPDX-License-Identifier: Apache-2.0
6
7// Licensed under the Apache License, Version 2.0 (the "License");
8// you may not use this file except in compliance with the License.
9// You may obtain a copy of the License at
10//
11// 	http://www.apache.org/licenses/LICENSE-2.0
12//
13// Unless required by applicable law or agreed to in writing, software
14// distributed under the License is distributed on an "AS IS" BASIS,
15// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16// See the License for the specific language governing permissions and
17// limitations under the License.
18
19//! Utility module to interact with solidity file.
20
21use sp_io::hashing::keccak_256;
22use std::{
23	collections::HashMap,
24	fs::File,
25	io::{BufRead, BufReader, Read},
26};
27
28pub fn check_precompile_implements_solidity_interfaces<F>(
29	files: &[&'static str],
30	supports_selector: F,
31) where
32	F: Fn(u32) -> bool,
33{
34	for file in files {
35		for solidity_fn in get_selectors(file) {
36			assert_eq!(
37				solidity_fn.compute_selector_hex(),
38				solidity_fn.docs_selector,
39				"documented selector for '{}' did not match in file '{}'",
40				solidity_fn.signature(),
41				file,
42			);
43
44			let selector = solidity_fn.compute_selector();
45			if !supports_selector(selector) {
46				panic!(
47					"precompile don't support selector {selector:x} for function '{}' listed in file\
48					{file}",
49					solidity_fn.signature(),
50				)
51			}
52		}
53	}
54}
55
56/// Represents a declared custom type struct within a solidity file
57#[derive(Clone, Default, Debug)]
58pub struct SolidityStruct {
59	/// Struct name
60	pub name: String,
61	/// List of parameter types
62	pub params: Vec<String>,
63	/// Is struct an enum
64	pub is_enum: bool,
65}
66
67impl SolidityStruct {
68	/// Returns the representative signature for the solidity struct
69	pub fn signature(&self) -> String {
70		if self.is_enum {
71			"uint8".to_string()
72		} else {
73			format!("({})", self.params.join(","))
74		}
75	}
76}
77
78/// Represents a declared function within a solidity file
79#[derive(Clone, Default)]
80pub struct SolidityFunction {
81	/// Function name
82	pub name: String,
83	/// List of function parameter types
84	pub args: Vec<String>,
85	/// The declared selector in the file
86	pub docs_selector: String,
87}
88
89impl SolidityFunction {
90	/// Returns the representative signature for the solidity function
91	pub fn signature(&self) -> String {
92		format!("{}({})", self.name, self.args.join(","))
93	}
94
95	/// Computes the selector code for the solidity function
96	pub fn compute_selector(&self) -> u32 {
97		compute_selector(&self.signature())
98	}
99
100	/// Computes the selector code as a hex string for the solidity function
101	pub fn compute_selector_hex(&self) -> String {
102		format!("{:0>8x}", self.compute_selector())
103	}
104}
105
106/// Computes a solidity selector from a given string
107pub fn compute_selector(v: &str) -> u32 {
108	let output = keccak_256(v.as_bytes());
109	let mut buf = [0u8; 4];
110	buf.clone_from_slice(&output[..4]);
111	u32::from_be_bytes(buf)
112}
113
114/// Returns a list of [SolidityFunction] defined in a solidity file
115pub fn get_selectors(filename: &str) -> Vec<SolidityFunction> {
116	let file =
117		File::open(filename).unwrap_or_else(|e| panic!("failed opening file '{filename}': {e}"));
118	get_selectors_from_reader(file)
119}
120
121/// Attempts to lookup a custom struct and returns its primitive signature
122fn try_lookup_custom_type(word: &str, custom_types: &HashMap<String, SolidityStruct>) -> String {
123	match word.strip_suffix("[]") {
124		Some(word) => {
125			if let Some(t) = custom_types.get(word) {
126				return format!("{}[]", t.signature());
127			}
128		}
129		None => {
130			if let Some(t) = custom_types.get(word) {
131				return t.signature();
132			}
133		}
134	};
135
136	word.to_string()
137}
138
139fn get_selectors_from_reader<R: Read>(reader: R) -> Vec<SolidityFunction> {
140	#[derive(Clone, Copy)]
141	enum Stage {
142		Start,
143		Enum,
144		Struct,
145		StructParams,
146		FnName,
147		Args,
148	}
149	#[derive(Clone, Copy)]
150	enum Pair {
151		First,
152		Second,
153	}
154	impl Pair {
155		fn next(&mut self) {
156			*self = match self {
157				Pair::First => Pair::Second,
158				Pair::Second => Pair::First,
159			}
160		}
161	}
162
163	let reader = BufReader::new(reader);
164	let mut functions = vec![];
165	let mut custom_types = HashMap::new();
166	let mut solidity_struct = SolidityStruct::default();
167
168	let mut stage = Stage::Start;
169	let mut pair = Pair::First;
170	let mut solidity_fn = SolidityFunction::default();
171	for line in reader.lines() {
172		let line = line.expect("failed unwrapping line").trim().to_string();
173		// identify declared selector
174		if line.starts_with("/// @custom:selector ") && matches!(stage, Stage::Start) {
175			solidity_fn.docs_selector = line.replace("/// @custom:selector ", "").to_string();
176		}
177
178		// skip comments
179		if line.starts_with("//") {
180			continue;
181		}
182
183		for word in line.split(&[';', ',', '(', ')', ' ']) {
184			// skip whitespace
185			if word.trim().is_empty() {
186				continue;
187			}
188			match (stage, pair, word) {
189				// parse custom type enums
190				(Stage::Start, Pair::First, "enum") => {
191					stage = Stage::Enum;
192					pair.next();
193				}
194				(Stage::Enum, Pair::Second, _) => {
195					custom_types.insert(
196						word.to_string(),
197						SolidityStruct {
198							name: word.to_string(),
199							is_enum: true,
200							params: vec![],
201						},
202					);
203					stage = Stage::Start;
204					pair = Pair::First;
205				}
206
207				// parse custom type structs
208				(Stage::Start, Pair::First, "struct") => {
209					stage = Stage::Struct;
210					pair.next();
211				}
212				(Stage::Struct, Pair::Second, _) => {
213					solidity_struct.name = word.to_string();
214					stage = Stage::StructParams;
215					pair.next();
216				}
217				(Stage::StructParams, Pair::First, "{") => (),
218				(Stage::StructParams, Pair::First, "}") => {
219					custom_types.insert(solidity_struct.name.clone(), solidity_struct);
220					stage = Stage::Start;
221					solidity_struct = SolidityStruct::default();
222				}
223				(Stage::StructParams, Pair::First, _) => {
224					let param = try_lookup_custom_type(word, &custom_types);
225					solidity_struct.params.push(param);
226					pair.next();
227				}
228				(Stage::StructParams, Pair::Second, _) => {
229					pair.next();
230				}
231
232				// parse function
233				(Stage::Start, Pair::First, "function") => {
234					stage = Stage::FnName;
235					pair.next();
236				}
237				(Stage::FnName, Pair::Second, _) => {
238					solidity_fn.name = word.to_string();
239					stage = Stage::Args;
240					pair.next();
241				}
242				(Stage::Args, Pair::First, "external") => {
243					functions.push(solidity_fn);
244					stage = Stage::Start;
245					pair = Pair::First;
246					solidity_fn = SolidityFunction::default()
247				}
248				(Stage::Args, Pair::First, _) => {
249					let mut arg = word.to_string();
250					arg = try_lookup_custom_type(&arg, &custom_types);
251
252					solidity_fn.args.push(arg);
253					pair.next();
254				}
255				(Stage::Args, Pair::Second, "memory" | "calldata" | "storage") => (),
256				(Stage::Args, Pair::Second, _) => pair.next(),
257				_ => {
258					stage = Stage::Start;
259					pair = Pair::First;
260					solidity_fn = SolidityFunction::default()
261				}
262			}
263		}
264	}
265
266	functions
267}
268
269#[cfg(test)]
270mod tests {
271	use super::*;
272
273	#[test]
274	fn test_selectors_are_parsed() {
275		let actual = get_selectors("tests/solidity_test.sol")
276			.into_iter()
277			.map(|sol_fn| {
278				(
279					sol_fn.compute_selector_hex(),
280					sol_fn.docs_selector.clone(),
281					sol_fn.signature(),
282				)
283			})
284			.collect::<Vec<_>>();
285		let expected = vec![
286			(
287				String::from("f7af8d91"),
288				String::from(""),
289				String::from("fnNoArgs()"),
290			),
291			(
292				String::from("d43a9a43"),
293				String::from("c4921133"),
294				String::from("fnOneArg(address)"),
295			),
296			(
297				String::from("40d6a43d"),
298				String::from("67ea837e"),
299				String::from("fnTwoArgs(address,uint256)"),
300			),
301			(
302				String::from("cee150c8"),
303				String::from("d6b423d9"),
304				String::from("fnSameArgs(uint64,uint64)"),
305			),
306			(
307				String::from("c6024207"),
308				String::from("b9904a86"),
309				String::from("fnOneArgSameLine(uint64)"),
310			),
311			(
312				String::from("fcbc04c3"),
313				String::from("28f0c44e"),
314				String::from("fnTwoArgsSameLine(uint64,bytes32)"),
315			),
316			(
317				String::from("c590304c"),
318				String::from("06f0c1ce"),
319				String::from("fnTwoArgsSameLineExternalSplit(uint64,bytes32)"),
320			),
321			(
322				String::from("a19a07e1"),
323				String::from("18001a4e"),
324				String::from("fnMemoryArrayArgs(address[],uint256[],bytes[])"),
325			),
326			(
327				String::from("ec26cf1c"),
328				String::from("1ea61a4e"),
329				String::from("fnCalldataArgs(string,bytes[])"),
330			),
331			(
332				String::from("f29f96de"),
333				String::from("d8af1a4e"),
334				String::from("fnCustomArgs((uint8,bytes[]),bytes[],uint64)"),
335			),
336			(
337				String::from("d751d651"),
338				String::from("e8af1642"),
339				String::from("fnEnumArgs(uint8,uint64)"),
340			),
341			(
342				String::from("b2c9f1a3"),
343				String::from("550c1a4e"),
344				String::from(
345					"fnCustomArgsMultiple((uint8,bytes[]),(address[],uint256[],bytes[]),bytes[],\
346					uint64)",
347				),
348			),
349			(
350				String::from("d5363eee"),
351				String::from("77af1a40"),
352				String::from("fnCustomArrayArgs((uint8,bytes[])[],bytes[])"),
353			),
354			(
355				String::from("b82da727"),
356				String::from("80af0a40"),
357				String::from(
358					"fnCustomComposedArg(((uint8,bytes[]),\
359				(address[],uint256[],bytes[])[]),uint64)",
360				),
361			),
362			(
363				String::from("586a2193"),
364				String::from("97baa040"),
365				String::from(
366					"fnCustomComposedArrayArg(((uint8,bytes[]),\
367				(address[],uint256[],bytes[])[])[],uint64)",
368				),
369			),
370		];
371
372		assert_eq!(expected, actual);
373	}
374}