precompile_utils/solidity/
revert.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//! Utilities to work with revert messages with support for backtraces and
20//! consistent formatting.
21
22use crate::solidity::{self, codec::bytes::UnboundedBytes};
23use alloc::{
24	string::{String, ToString},
25	vec::Vec,
26};
27use fp_evm::{ExitRevert, PrecompileFailure};
28
29/// Represent the result of a computation that can revert.
30pub type MayRevert<T = ()> = Result<T, Revert>;
31
32/// Generate an encoded revert from a simple String.
33/// Returns a `PrecompileFailure` that fits in an `EvmResult::Err`.
34pub fn revert(msg: impl Into<String>) -> PrecompileFailure {
35	RevertReason::custom(msg).into()
36}
37
38/// Generate an encoded revert from a simple String.
39/// Returns a `Vec<u8>` in case `PrecompileFailure` is too high level.
40pub fn revert_as_bytes(msg: impl Into<String>) -> Vec<u8> {
41	Revert::new(RevertReason::custom(msg)).to_encoded_bytes()
42}
43
44/// Generic error to build abi-encoded revert output.
45/// See: https://docs.soliditylang.org/en/latest/control-structures.html?highlight=revert#revert
46pub const ERROR_SELECTOR: u32 = 0x08c379a0;
47
48#[derive(Clone, PartialEq, Eq)]
49enum BacktracePart {
50	Field(String),
51	Tuple(usize),
52	Array(usize),
53}
54
55/// Backtrace of an revert.
56/// Built depth-first.
57/// Implement `Display` to render the backtrace as a string.
58#[derive(Default, PartialEq, Eq)]
59pub struct Backtrace(Vec<BacktracePart>);
60
61impl Backtrace {
62	/// Create a new empty backtrace.
63	pub fn new() -> Self {
64		Self(Vec::new())
65	}
66
67	/// Check if the backtrace is empty.
68	pub fn is_empty(&self) -> bool {
69		self.0.is_empty()
70	}
71}
72
73impl core::fmt::Display for Backtrace {
74	fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> {
75		for (i, part) in self.0.iter().rev().enumerate() {
76			match (i, part) {
77				(0, BacktracePart::Field(field)) => write!(f, "{field}")?,
78				(_, BacktracePart::Field(field)) => write!(f, ".{field}")?,
79				(_, BacktracePart::Tuple(index)) => write!(f, ".{index}")?,
80				(_, BacktracePart::Array(index)) => write!(f, "[{index}]")?,
81			}
82		}
83		Ok(())
84	}
85}
86
87/// Possible revert reasons.
88#[non_exhaustive]
89#[derive(PartialEq, Eq)]
90pub enum RevertReason {
91	/// A custom revert reason if other variants are not appropriate.
92	Custom(String),
93	/// Tried to read data out of bounds.
94	ReadOutOfBounds {
95		/// What was being read?
96		what: String,
97	},
98	/// An unknown selector has been provided.
99	UnknownSelector,
100	/// A value is too large to fit in the wanted type.
101	/// For security reasons integers are always parsed as `uint256` then
102	/// casted to the wanted type. If the value overflows this type then this
103	/// revert is used.
104	ValueIsTooLarge {
105		/// What was being read?
106		what: String,
107	},
108	/// A pointer (used for structs and arrays) points out of bounds.
109	PointerToOutofBound,
110	/// The reading cursor overflowed.
111	/// This should realistically never happen as it would require an input
112	/// of length larger than 2^64, which would cost too much to be included
113	/// in a block.
114	CursorOverflow,
115	/// Used by a check that the input contains at least N static arguments.
116	/// Often use to return early if the input is too short.
117	ExpectedAtLeastNArguments(usize),
118}
119
120impl RevertReason {
121	/// Create a `RevertReason::Custom` from anything that can be converted to a `String`.
122	/// Argument is the custom revert message.
123	pub fn custom(s: impl Into<String>) -> Self {
124		RevertReason::Custom(s.into())
125	}
126
127	/// Create a `RevertReason::ReadOutOfBounds` from anything that can be converted to a `String`.
128	/// Argument names what was expected to be read.
129	pub fn read_out_of_bounds(what: impl Into<String>) -> Self {
130		RevertReason::ReadOutOfBounds { what: what.into() }
131	}
132
133	/// Create a `RevertReason::ValueIsTooLarge` from anything that can be converted to a `String`.
134	/// Argument names what was expected to be read.
135	pub fn value_is_too_large(what: impl Into<String>) -> Self {
136		RevertReason::ValueIsTooLarge { what: what.into() }
137	}
138}
139
140impl core::fmt::Display for RevertReason {
141	fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> {
142		match self {
143			RevertReason::Custom(s) => write!(f, "{s}"),
144			RevertReason::ReadOutOfBounds { what } => {
145				write!(f, "Tried to read {what} out of bounds")
146			}
147			RevertReason::UnknownSelector => write!(f, "Unknown selector"),
148			RevertReason::ValueIsTooLarge { what } => write!(f, "Value is too large for {what}"),
149			RevertReason::PointerToOutofBound => write!(f, "Pointer points to out of bound"),
150			RevertReason::CursorOverflow => write!(f, "Reading cursor overflowed"),
151			RevertReason::ExpectedAtLeastNArguments(n) => {
152				write!(f, "Expected at least {n} arguments")
153			}
154		}
155	}
156}
157
158/// An revert returned by various functions in precompile-utils.
159/// Allows to dynamically construct the backtrace (backtrace) of the revert
160/// and manage it in a typed way.
161/// Can be transformed into a `PrecompileFailure::Revert` and `String`, and
162/// implement `Display` and `Debug`.
163#[derive(PartialEq, Eq)]
164pub struct Revert {
165	reason: RevertReason,
166	backtrace: Backtrace,
167}
168
169impl Revert {
170	/// Create a new `Revert` with a `RevertReason` and
171	/// an empty backtrace.
172	pub fn new(reason: RevertReason) -> Self {
173		Self {
174			reason,
175			backtrace: Backtrace::new(),
176		}
177	}
178
179	/// For all `RevertReason` variants that have a `what` field, change its value.
180	/// Otherwise do nothing.
181	/// It is useful when writing custom types `solidity::Codec` implementations using
182	/// simpler types.
183	pub fn change_what(mut self, what: impl Into<String>) -> Self {
184		let what = what.into();
185
186		self.reason = match self.reason {
187			RevertReason::ReadOutOfBounds { .. } => RevertReason::ReadOutOfBounds { what },
188			RevertReason::ValueIsTooLarge { .. } => RevertReason::ValueIsTooLarge { what },
189			other => other,
190		};
191
192		self
193	}
194
195	/// Transforms the revert into its bytes representation (from a String).
196	pub fn to_encoded_bytes(self) -> Vec<u8> {
197		let bytes: Vec<u8> = self.into();
198		solidity::encode_with_selector(ERROR_SELECTOR, UnboundedBytes::from(bytes))
199	}
200}
201
202impl From<RevertReason> for Revert {
203	fn from(a: RevertReason) -> Revert {
204		Revert::new(a)
205	}
206}
207
208impl core::fmt::Display for Revert {
209	fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> {
210		if !self.backtrace.is_empty() {
211			write!(f, "{}: ", self.backtrace)?;
212		}
213
214		write!(f, "{}", self.reason)
215	}
216}
217
218impl core::fmt::Debug for Revert {
219	fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> {
220		write!(f, "{self}")
221	}
222}
223
224impl From<Revert> for Vec<u8> {
225	fn from(val: Revert) -> Self {
226		val.to_string().into()
227	}
228}
229
230/// Allows to inject backtrace data.
231pub trait InjectBacktrace {
232	/// Output type of the injection.
233	/// Should be a type that can hold a backtrace.
234	type Output;
235
236	/// Occurs in a field.
237	fn in_field(self, field: impl Into<String>) -> Self::Output;
238
239	/// Occurs in a tuple.
240	fn in_tuple(self, index: usize) -> Self::Output;
241
242	/// Occurs in an array at provided index.
243	fn in_array(self, index: usize) -> Self::Output;
244}
245
246/// Additional function for everything having a Backtrace.
247pub trait BacktraceExt {
248	/// Map last tuple entry into a field.
249	/// Does nothing if last entry is not a tuple.
250	/// As in Solidity structs are equivalent to tuples and are tricky to parse correctly,
251	/// it allows to parse any struct as a tuple (with the correct implementation in this crate) and
252	/// then map tuple indices to struct fields.
253	fn map_in_tuple_to_field(self, fields: &[&'static str]) -> Self;
254}
255
256/// Additional functions for Revert and MayRevert.
257pub trait RevertExt {
258	/// Map the reason while keeping the same backtrace.
259	fn map_reason(self, f: impl FnOnce(RevertReason) -> RevertReason) -> Self;
260}
261
262impl InjectBacktrace for RevertReason {
263	// `RevertReason` cannot hold a backtrace, thus it wraps
264	// it into a `Revert`.
265	type Output = Revert;
266
267	fn in_field(self, field: impl Into<String>) -> Revert {
268		Revert::new(self).in_field(field)
269	}
270
271	fn in_array(self, index: usize) -> Revert {
272		Revert::new(self).in_array(index)
273	}
274
275	fn in_tuple(self, index: usize) -> Revert {
276		Revert::new(self).in_tuple(index)
277	}
278}
279
280impl InjectBacktrace for Backtrace {
281	type Output = Self;
282
283	fn in_field(mut self, field: impl Into<String>) -> Self {
284		self.0.push(BacktracePart::Field(field.into()));
285		self
286	}
287
288	fn in_array(mut self, index: usize) -> Self {
289		self.0.push(BacktracePart::Array(index));
290		self
291	}
292
293	fn in_tuple(mut self, index: usize) -> Self {
294		self.0.push(BacktracePart::Tuple(index));
295		self
296	}
297}
298
299impl BacktraceExt for Backtrace {
300	fn map_in_tuple_to_field(mut self, fields: &[&'static str]) -> Self {
301		if let Some(entry) = self.0.last_mut() {
302			if let BacktracePart::Tuple(index) = *entry {
303				if let Some(field) = fields.get(index) {
304					*entry = BacktracePart::Field(field.to_string())
305				}
306			}
307		}
308		self
309	}
310}
311
312impl InjectBacktrace for Revert {
313	type Output = Self;
314
315	fn in_field(mut self, field: impl Into<String>) -> Self {
316		self.backtrace = self.backtrace.in_field(field);
317		self
318	}
319
320	fn in_array(mut self, index: usize) -> Self {
321		self.backtrace = self.backtrace.in_array(index);
322		self
323	}
324
325	fn in_tuple(mut self, index: usize) -> Self {
326		self.backtrace = self.backtrace.in_tuple(index);
327		self
328	}
329}
330
331impl RevertExt for Revert {
332	fn map_reason(mut self, f: impl FnOnce(RevertReason) -> RevertReason) -> Self {
333		self.reason = f(self.reason);
334		self
335	}
336}
337
338impl BacktraceExt for Revert {
339	fn map_in_tuple_to_field(mut self, fields: &[&'static str]) -> Self {
340		self.backtrace = self.backtrace.map_in_tuple_to_field(fields);
341		self
342	}
343}
344
345impl<T> InjectBacktrace for MayRevert<T> {
346	type Output = Self;
347
348	fn in_field(self, field: impl Into<String>) -> Self {
349		self.map_err(|e| e.in_field(field))
350	}
351
352	fn in_array(self, index: usize) -> Self {
353		self.map_err(|e| e.in_array(index))
354	}
355
356	fn in_tuple(self, index: usize) -> Self {
357		self.map_err(|e| e.in_tuple(index))
358	}
359}
360
361impl<T> RevertExt for MayRevert<T> {
362	fn map_reason(self, f: impl FnOnce(RevertReason) -> RevertReason) -> Self {
363		self.map_err(|e| e.map_reason(f))
364	}
365}
366
367impl<T> BacktraceExt for MayRevert<T> {
368	fn map_in_tuple_to_field(self, fields: &[&'static str]) -> Self {
369		self.map_err(|e| e.map_in_tuple_to_field(fields))
370	}
371}
372
373impl From<Revert> for PrecompileFailure {
374	fn from(err: Revert) -> Self {
375		PrecompileFailure::Revert {
376			exit_status: ExitRevert::Reverted,
377			output: err.to_encoded_bytes(),
378		}
379	}
380}
381
382impl From<RevertReason> for PrecompileFailure {
383	fn from(err: RevertReason) -> Self {
384		Revert::new(err).into()
385	}
386}