neo_solidity/ir/build/module/
hazards.rs

1#[derive(Default, Clone, Copy)]
2struct Hazards {
3    writes_state: bool,
4    notifies: bool,
5    unsafe_contract_call: bool,
6    reads_state: bool,
7    reads_environment: bool,
8    contract_calls: bool,
9}
10
11impl Hazards {
12    fn safe_violation(self) -> bool {
13        self.writes_state || self.notifies || self.unsafe_contract_call
14    }
15
16    fn pure_violation(self) -> bool {
17        self.safe_violation()
18            || self.reads_state
19            || self.reads_environment
20            || self.contract_calls
21    }
22
23    fn merge_in(&mut self, other: Hazards) {
24        self.writes_state |= other.writes_state;
25        self.notifies |= other.notifies;
26        self.unsafe_contract_call |= other.unsafe_contract_call;
27        self.reads_state |= other.reads_state;
28        self.reads_environment |= other.reads_environment;
29        self.contract_calls |= other.contract_calls;
30    }
31}
32
33fn direct_hazards(function: &Function) -> Hazards {
34    let instrs: Vec<&Instruction> = function
35        .basic_blocks
36        .iter()
37        .flat_map(|block| block.instructions.iter())
38        .collect();
39
40    let mut hazards = Hazards::default();
41
42    for (index, instr) in instrs.iter().enumerate() {
43        match instr {
44            Instruction::LoadState(_)
45            | Instruction::LoadMappingElement { .. }
46            | Instruction::LoadStructField { .. }
47            | Instruction::LoadStructArrayElement { .. }
48            | Instruction::LoadStorageDynamic => {
49                hazards.reads_state = true;
50            }
51            Instruction::StoreState(_)
52            | Instruction::StoreMappingElement { .. }
53            | Instruction::StoreStructField { .. }
54            | Instruction::StoreStructArrayElement { .. } => {
55                hazards.writes_state = true;
56            }
57            Instruction::LoadRuntimeValue(_) => {
58                hazards.reads_environment = true;
59            }
60            Instruction::EmitEvent { .. } | Instruction::EmitEventByName { .. } => {
61                hazards.notifies = true;
62            }
63            Instruction::CallBuiltin { builtin, arg_count } => match builtin {
64                BuiltinCall::StorageGet | BuiltinCall::StorageFind => {
65                    hazards.reads_state = true;
66                }
67                BuiltinCall::StoragePut | BuiltinCall::StorageDelete => {
68                    hazards.writes_state = true;
69                }
70                BuiltinCall::RuntimeNotify | BuiltinCall::NotifySerialized => {
71                    hazards.notifies = true;
72                }
73                BuiltinCall::RuntimeCheckWitness => {
74                    hazards.reads_environment = true;
75                }
76                BuiltinCall::DeployContract => {
77                    hazards.writes_state = true;
78                    hazards.contract_calls = true;
79                }
80                BuiltinCall::GetContract
81                | BuiltinCall::GetContractScript
82                | BuiltinCall::GetNeoAccountState => {
83                    hazards.contract_calls = true;
84                }
85                BuiltinCall::NativeCall { contract, method } => {
86                    if native_call_is_mutating(*contract, method) {
87                        hazards.writes_state = true;
88                    }
89                    if !matches!(contract, NativeContract::CryptoLib | NativeContract::StdLib) {
90                        hazards.contract_calls = true;
91                    }
92                }
93                BuiltinCall::Syscall(name) => {
94                    // Disallow syscalls that obviously mutate state or send notifications.
95                    if matches!(
96                        name.as_str(),
97                        "System.Storage.Put"
98                            | "System.Storage.Delete"
99                            | "System.Storage.Local.Put"
100                            | "System.Storage.Local.Delete"
101                    ) {
102                        hazards.writes_state = true;
103                    }
104                    if matches!(name.as_str(), "System.Runtime.Notify" | "System.Runtime.Log") {
105                        hazards.notifies = true;
106                    }
107
108                    // Storage syscalls read contract storage (including read-only contexts).
109                    if matches!(
110                        name.as_str(),
111                        "System.Storage.Get"
112                            | "System.Storage.Find"
113                            | "System.Storage.GetContext"
114                            | "System.Storage.GetReadOnlyContext"
115                            | "System.Storage.Local.GetContext"
116                            | "System.Storage.Local.GetReadOnlyContext"
117                    ) {
118                        hazards.reads_state = true;
119                    }
120
121                    // Runtime/environment syscalls.
122                    if name.starts_with("System.Runtime.")
123                        && !matches!(
124                            name.as_str(),
125                            "System.Runtime.Notify" | "System.Runtime.Log"
126                        )
127                    {
128                        hazards.reads_environment = true;
129                    }
130
131                    // Iterators are produced by storage queries; treat them as state reads.
132                    if matches!(name.as_str(), "System.Iterator.Next" | "System.Iterator.Value") {
133                        hazards.reads_state = true;
134                    }
135
136                    // Track contract calls that grant write/notify permissions so that safe
137                    // Solidity methods cannot indirectly perform stateful cross-contract calls.
138                    if name == "System.Contract.Call" {
139                        hazards.contract_calls = true;
140                    }
141                    if name == "System.Contract.Call" && *arg_count == 4 {
142                        let flags = match instrs.get(index.wrapping_sub(3)) {
143                            Some(Instruction::PushLiteral(LiteralValue::Integer(value))) => {
144                                parse_u8_literal(value)
145                            }
146                            _ => None,
147                        };
148                        if flags.is_none_or(call_flags_allow_write_or_notify) {
149                            hazards.unsafe_contract_call = true;
150                        }
151                    }
152                }
153                BuiltinCall::ContractCall => {
154                    // ContractCall uses CallFlags.All in the bytecode emitter.
155                    hazards.contract_calls = true;
156                    hazards.unsafe_contract_call = true;
157                }
158                BuiltinCall::ContractCallWithFlags => {
159                    hazards.contract_calls = true;
160                    let flags = match instrs.get(index.wrapping_sub(1)) {
161                        Some(Instruction::PushLiteral(LiteralValue::Integer(value))) => {
162                            parse_u8_literal(value)
163                        }
164                        _ => None,
165                    };
166                    if flags.is_none_or(call_flags_allow_write_or_notify) {
167                        hazards.unsafe_contract_call = true;
168                    }
169                }
170                _ => {}
171            },
172            _ => {}
173        }
174    }
175
176    hazards
177}
178
179fn build_call_graph(functions: &[Function]) -> HashMap<String, Vec<String>> {
180    let mut graph: HashMap<String, Vec<String>> = HashMap::new();
181    for function in functions {
182        let mut callees: Vec<String> = Vec::new();
183        for instr in function
184            .basic_blocks
185            .iter()
186            .flat_map(|block| block.instructions.iter())
187        {
188            if let Instruction::CallFunction { name, .. } = instr {
189                callees.push(name.clone());
190            }
191        }
192        graph.insert(function.name.clone(), callees);
193    }
194    graph
195}
196
197fn compute_transitive_hazards(
198    name: &str,
199    direct: &HashMap<String, Hazards>,
200    graph: &HashMap<String, Vec<String>>,
201    memo: &mut HashMap<String, Hazards>,
202    visiting: &mut HashSet<String>,
203) -> Hazards {
204    if let Some(value) = memo.get(name).copied() {
205        return value;
206    }
207
208    if visiting.contains(name) {
209        return direct.get(name).copied().unwrap_or_default();
210    }
211
212    visiting.insert(name.to_string());
213
214    let mut hazards = direct.get(name).copied().unwrap_or_default();
215    if let Some(callees) = graph.get(name) {
216        for callee in callees {
217            let child = compute_transitive_hazards(callee, direct, graph, memo, visiting);
218            hazards.merge_in(child);
219        }
220    }
221
222    visiting.remove(name);
223    memo.insert(name.to_string(), hazards);
224    hazards
225}