bobcat_panic/
lib.rs

1#![cfg_attr(not(feature = "std"), no_std)]
2
3#[cfg(feature = "alloc")]
4extern crate alloc;
5
6#[allow(unused)]
7use core::fmt::{Result as FmtResult, Write};
8
9use paste::paste;
10
11#[cfg(all(target_family = "wasm", target_os = "unknown"))]
12mod impls {
13    #[link(wasm_import_module = "console")]
14    #[cfg(feature = "console")]
15    unsafe extern "C" {
16        pub(crate) fn log_txt(ptr: *const u8, len: usize);
17    }
18
19    #[cfg(all(target_family = "wasm", target_os = "unknown"))]
20    #[link(wasm_import_module = "vm_hooks")]
21    #[allow(unused)]
22    unsafe extern "C" {
23        pub(crate) fn exit_early(code: i32) -> !;
24        pub(crate) fn write_result(d: *const u8, l: usize);
25        pub(crate) fn transient_store_bytes32(key: *const u8, value: *const u8);
26        pub(crate) fn transient_load_bytes32(key: *const u8, value: *const u8);
27    }
28}
29
30#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
31mod impls {
32    pub unsafe fn transient_store_bytes32(_: *const u8, _: *const u8) {}
33    pub unsafe fn transient_load_bytes32(_: *const u8, _: *const u8) {}
34}
35
36#[cfg(all(target_family = "wasm", target_os = "unknown"))]
37fn write_result_slice(s: &[u8]) {
38    unsafe { impls::write_result(s.as_ptr(), s.len()) }
39}
40
41//Panic(uint256)
42pub const PANIC_PREAMBLE_WORD: [u8; 32 + 4] = match const_hex::const_decode_to_array::<{ 32 + 4 }>(
43    b"4e487b710000000000000000000000000000000000000000000000000000000000000000",
44) {
45    Ok(v) => v,
46    Err(_) => panic!(),
47};
48
49//Error(string)
50pub const ERROR_PREAMBLE_OFFSET: [u8; 4 + 32] = match const_hex::const_decode_to_array::<{ 4 + 32 }>(
51    b"08c379a00000000000000000000000000000000000000000000000000000000000000020",
52) {
53    Ok(v) => v,
54    Err(_) => panic!(),
55};
56
57#[derive(Clone, Debug, PartialEq)]
58#[repr(u8)]
59pub enum PanicCodes {
60    DecodingError = 0,
61    OverflowOrUnderflow = 0x11,
62    DivByZero = 0x12,
63}
64
65#[cfg(all(target_family = "wasm", target_os = "unknown"))]
66pub fn panic_with_code(x: PanicCodes) -> ! {
67    let mut b = PANIC_PREAMBLE_WORD;
68    b[4 + 32 - 1] = x as u8;
69    write_result_slice(&b);
70    unsafe { impls::exit_early(1) }
71}
72
73#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
74pub fn panic_with_code(x: PanicCodes) -> ! {
75    panic!("panicked with code: {x:?}");
76}
77
78#[macro_export]
79macro_rules! define_panic_macros {
80    (
81        $(($error_msg:expr, $panic_code:ident)),* $(,)?
82    ) => {
83        $(
84            paste! {
85                #[macro_export]
86                macro_rules! [<panic_on_err_ $error_msg>] {
87                    ($e:expr, $msg:expr) => {{
88                        match $e {
89                            Some(v) => v,
90                            None => {
91                                #[cfg(feature = "msg-on-sdk-err")]
92                                panic!("{}: {}", $error_msg, $msg);
93                                #[cfg(not(feature = "msg-on-sdk-err"))]
94                                $crate::panic_with_code($crate::PanicCodes::$panic_code);
95                            }
96                        }
97                    }};
98                }
99            }
100        )*
101    };
102}
103
104define_panic_macros!(
105    ("overflow", OverflowOrUnderflow),
106    ("div_by_zero", DivByZero),
107);
108
109#[macro_export]
110macro_rules! panic_on_err_bad_decoding_bool {
111    ($msg:expr) => {{
112        #[cfg(feature = "msg-on-sdk-err")]
113        panic!("error decoding: {}", $msg);
114        #[cfg(not(feature = "msg-on-sdk-err"))]
115        $crate::panic_with_code($crate::PanicCodes::DecodingError);
116    }};
117    ($e:expr, $msg:expr) => {{
118        if !$e {
119            panic_on_err_bad_decoding_bool!($msg);
120        }
121    }};
122}
123
124#[allow(unused)]
125struct SliceWriter<'a>(&'a mut [u8], usize);
126
127impl<'a> Write for SliceWriter<'a> {
128    fn write_str(&mut self, s: &str) -> FmtResult {
129        let v = s.len().min(REVERT_BUF_SIZE.saturating_sub(self.1));
130        self.0[self.1..self.1 + v].copy_from_slice(&s.as_bytes()[..v]);
131        self.1 += v;
132        Ok(())
133    }
134}
135
136//uint256(keccak256(abi.encodePacked("bobcat.tracing.counter"))) - 1
137pub const SLOT_TRACING_COUNTER: [u8; 32] = [
138    0xad, 0x59, 0xcd, 0x5c, 0xcd, 0xcd, 0x00, 0x59, 0x2c, 0xd2, 0x06, 0xdc, 0x3b, 0xce, 0x83, 0xac,
139    0xe6, 0x1b, 0x8c, 0x80, 0xcb, 0xe9, 0xfd, 0x0d, 0x70, 0x09, 0x34, 0xba, 0x13, 0x78, 0x92, 0x22,
140];
141
142/// Revert buffer size that's used to write the panic. We can afford to
143/// use a large page here since a panic will consume all the gas anyway,
144/// and a user will see this during simulation hopefully.
145#[allow(unused)]
146const REVERT_BUF_SIZE: usize = 1024;
147
148#[cfg(all(feature = "panic-revert", feature = "panic-loc"))]
149compile_error!("panic-revert and panic-loc simultaneously enabled");
150
151#[cfg(all(feature = "panic-revert", feature = "panic-trace"))]
152compile_error!("panic-revert and panic-trace simultaneously enabled");
153
154#[cfg(all(feature = "panic-loc", feature = "panic-trace"))]
155compile_error!("panic-loc and panic-trace simultaneously enabled");
156
157#[derive(Debug, Clone, Copy, PartialEq)]
158#[repr(u8)]
159enum TracingDiscriminant {
160    Number = 0,
161    String = 1,
162}
163
164#[cfg(all(target_family = "wasm", target_os = "unknown"))]
165#[cfg_attr(all(feature = "panic", not(feature = "std")), panic_handler)]
166pub fn panic_handler(_msg: &core::panic::PanicInfo) -> ! {
167    #[cfg(feature = "console")]
168    {
169        let msg = alloc::format!("{_msg}");
170        unsafe { impls::log_txt(msg.as_ptr(), msg.len()) }
171    }
172    #[cfg(any(
173        feature = "panic-revert",
174        feature = "panic-loc",
175        feature = "panic-trace"
176    ))]
177    {
178        let mut buf = [0u8; REVERT_BUF_SIZE];
179        buf[..ERROR_PREAMBLE_OFFSET.len()].copy_from_slice(&ERROR_PREAMBLE_OFFSET);
180        let mut w = SliceWriter(&mut buf[ERROR_PREAMBLE_OFFSET.len() + 32..], 0);
181        #[cfg(feature = "panic-revert")]
182        {
183            write!(&mut w, "{_msg}").unwrap();
184        }
185        #[cfg(feature = "panic-loc")]
186        if let Some(loc) = _msg.location() {
187            write!(&mut w, "panic: {}:{}", loc.file(), loc.line()).unwrap();
188        } else {
189            write!(&mut w, "panic: unknown").unwrap();
190        }
191        #[cfg(feature = "panic-trace")]
192        {
193            let mut b = [0u8; 32];
194            unsafe { impls::transient_load_bytes32(SLOT_TRACING_COUNTER.as_ptr(), b.as_mut_ptr()) };
195            if b[0] == TracingDiscriminant::Number as u8 {
196                write!(
197                    &mut w,
198                    "trace no: {}",
199                    u32::from_be_bytes(b[1..32 - size_of::<u32>()].try_into().unwrap())
200                )
201            } else {
202                write!(&mut w, "trace str: {}", trace_key_to_str(&b))
203            }
204            .unwrap()
205        }
206        let len_msg = w.1;
207        let len_offset = ERROR_PREAMBLE_OFFSET.len();
208        buf[len_offset + 28..len_offset + 32].copy_from_slice(&(len_msg as u32).to_be_bytes());
209        let len_full = ERROR_PREAMBLE_OFFSET.len() + 32 + len_msg;
210        let len_padded = len_full + (32 - (len_full % 32)) % 32;
211        write_result_slice(&buf[..len_padded]);
212        unsafe { impls::exit_early(1) }
213    }
214    // Prefer the normal behaviour if the user hasn't opted into this
215    // feature. Maybe it's better to wipe out the revertdata if this happens,
216    // the other behaviour is different.
217    #[allow(unreachable_code)]
218    core::arch::wasm32::unreachable()
219}
220
221pub fn bump() {
222    let p = SLOT_TRACING_COUNTER.as_ptr();
223    // We assume the execution counter here is always less than u32,
224    // so the upper part of the word could be dirty!
225    let mut b = [0u8; 32];
226    unsafe { impls::transient_load_bytes32(p, b.as_mut_ptr()) };
227    let v = u32::from_be_bytes(b[32 - size_of::<u32>()..].try_into().unwrap()) + 1;
228    b[32 - size_of::<u32>()..].copy_from_slice(&v.to_be_bytes());
229    b[0] = TracingDiscriminant::Number as u8;
230    unsafe { impls::transient_store_bytes32(p, b.as_ptr()) }
231}
232
233pub const fn trace_key_of_str(s: &str) -> [u8; 32] {
234    let bytes = s.as_bytes();
235    let mut b = [0u8; 32];
236    b[0] = TracingDiscriminant::String as u8;
237    let mut i = 0;
238    while i < bytes.len() && i < 31 {
239        b[i + 1] = bytes[i];
240        i += 1;
241    }
242    b
243}
244
245#[allow(unused)]
246const fn trace_key_to_str(b: &[u8; 32]) -> &str {
247    let mut i = 1;
248    while i < 32 {
249        if b[i] == 0 {
250            break;
251        }
252        i += 1;
253    }
254    unsafe {
255        let slice = core::slice::from_raw_parts(b.as_ptr().add(1), i - 1);
256        core::str::from_utf8_unchecked(slice)
257    }
258}
259
260pub fn trace(k: &str) {
261    let v = trace_key_of_str(k);
262    unsafe { impls::transient_store_bytes32(SLOT_TRACING_COUNTER.as_ptr(), v.as_ptr()) }
263}
264
265#[macro_export]
266macro_rules! trace_guard {
267    ($($body:tt)*) => {{
268        trace(concat!(file!(), ":", line!()));
269        $($body)*
270    }};
271}
272
273#[cfg(all(test, feature = "std"))]
274mod test {
275    use proptest::prelude::*;
276
277    use super::*;
278
279    proptest! {
280        #[test]
281        fn test_key_back_and_forth(x in proptest::string::string_regex("[0-9a-zA-Z]{0,31}").unwrap()) {
282            assert_eq!(&x, trace_key_to_str(&trace_key_of_str(&x)));
283        }
284    }
285}