#[derive(Default, Clone, Copy)]
struct Hazards {
writes_state: bool,
notifies: bool,
unsafe_contract_call: bool,
reads_state: bool,
reads_environment: bool,
contract_calls: bool,
}
impl Hazards {
fn safe_violation(self) -> bool {
self.writes_state || self.notifies || self.unsafe_contract_call
}
fn pure_violation(self) -> bool {
self.safe_violation()
|| self.reads_state
}
fn merge_in(&mut self, other: Hazards) {
self.writes_state |= other.writes_state;
self.notifies |= other.notifies;
self.unsafe_contract_call |= other.unsafe_contract_call;
self.reads_state |= other.reads_state;
self.reads_environment |= other.reads_environment;
self.contract_calls |= other.contract_calls;
}
}
fn direct_hazards(function: &Function) -> Hazards {
let instrs: Vec<&Instruction> = function
.basic_blocks
.iter()
.flat_map(|block| block.instructions.iter())
.collect();
let mut hazards = Hazards::default();
for (index, instr) in instrs.iter().enumerate() {
match instr {
Instruction::LoadState(_)
| Instruction::LoadMappingElement { .. }
| Instruction::LoadStructField { .. }
| Instruction::LoadStructArrayElement { .. }
| Instruction::LoadStructFieldMappingElement { .. }
| Instruction::LoadStorageDynamic => {
hazards.reads_state = true;
}
Instruction::StoreState(_)
| Instruction::StoreMappingElement { .. }
| Instruction::StoreArrayDeepCopy { .. }
| Instruction::StoreStructField { .. }
| Instruction::StoreStructArrayElement { .. }
| Instruction::StoreStructFieldMappingElement { .. } => {
hazards.writes_state = true;
}
Instruction::LoadRuntimeValue(_) => {
hazards.reads_environment = true;
}
Instruction::EmitEvent { .. } | Instruction::EmitEventByName { .. } => {
hazards.notifies = true;
}
Instruction::CallBuiltin { builtin, arg_count } => match builtin {
BuiltinCall::StorageGet | BuiltinCall::StorageFind => {
hazards.reads_state = true;
}
BuiltinCall::StoragePut | BuiltinCall::StorageDelete => {
hazards.writes_state = true;
}
BuiltinCall::RuntimeNotify | BuiltinCall::NotifySerialized => {
hazards.notifies = true;
}
BuiltinCall::RuntimeCheckWitness => {
hazards.reads_environment = true;
}
BuiltinCall::DeployContract => {
hazards.writes_state = true;
hazards.contract_calls = true;
}
BuiltinCall::GetContract
| BuiltinCall::GetContractScript
| BuiltinCall::GetNeoAccountState => {
hazards.contract_calls = true;
}
BuiltinCall::NativeCall { contract, method } => {
if native_call_is_mutating(*contract, method) {
hazards.writes_state = true;
}
if !matches!(contract, NativeContract::CryptoLib | NativeContract::StdLib) {
hazards.contract_calls = true;
}
}
BuiltinCall::Syscall(name) => {
if matches!(
name.as_str(),
"System.Storage.Put" | "System.Storage.Delete"
) {
hazards.writes_state = true;
}
if matches!(name.as_str(), "System.Runtime.Notify" | "System.Runtime.Log") {
hazards.notifies = true;
}
if matches!(
name.as_str(),
"System.Storage.Get"
| "System.Storage.Find"
| "System.Storage.GetContext"
| "System.Storage.GetReadOnlyContext"
) {
hazards.reads_state = true;
}
if name.starts_with("System.Runtime.")
&& !matches!(
name.as_str(),
"System.Runtime.Notify" | "System.Runtime.Log"
)
{
hazards.reads_environment = true;
}
if matches!(name.as_str(), "System.Iterator.Next" | "System.Iterator.Value") {
hazards.reads_state = true;
}
if name == "System.Contract.Call" {
hazards.contract_calls = true;
}
if name == "System.Contract.Call" && *arg_count == 4 {
let flags = match instrs.get(index.wrapping_sub(3)) {
Some(Instruction::PushLiteral(LiteralValue::Integer(value))) => {
parse_u8_literal(value)
}
_ => None,
};
if flags.is_none_or(call_flags_allow_write_or_notify) {
hazards.unsafe_contract_call = true;
}
}
}
BuiltinCall::ContractCall => {
hazards.contract_calls = true;
hazards.unsafe_contract_call = true;
}
BuiltinCall::ContractCallWithFlags => {
hazards.contract_calls = true;
let flags = match instrs.get(index.wrapping_sub(1)) {
Some(Instruction::PushLiteral(LiteralValue::Integer(value))) => {
parse_u8_literal(value)
}
_ => None,
};
if flags.is_none_or(call_flags_allow_write_or_notify) {
hazards.unsafe_contract_call = true;
}
}
_ => {}
},
_ => {}
}
}
hazards
}
fn build_call_graph(functions: &[Function]) -> HashMap<String, Vec<String>> {
let mut graph: HashMap<String, Vec<String>> = HashMap::new();
for function in functions {
let mut callees: Vec<String> = Vec::new();
for instr in function
.basic_blocks
.iter()
.flat_map(|block| block.instructions.iter())
{
match instr {
Instruction::CallFunction { name, .. } => callees.push(name.clone()),
Instruction::PushFunctionOffset { name } => callees.push(name.clone()),
_ => {}
}
}
graph.insert(function.name.clone(), callees);
}
graph
}
fn compute_transitive_hazards(
name: &str,
direct: &HashMap<String, Hazards>,
graph: &HashMap<String, Vec<String>>,
memo: &mut HashMap<String, Hazards>,
visiting: &mut HashSet<String>,
) -> Hazards {
if let Some(value) = memo.get(name).copied() {
return value;
}
if visiting.contains(name) {
return direct.get(name).copied().unwrap_or_default();
}
visiting.insert(name.to_string());
let mut hazards = direct.get(name).copied().unwrap_or_default();
if let Some(callees) = graph.get(name) {
for callee in callees {
let child = compute_transitive_hazards(callee, direct, graph, memo, visiting);
hazards.merge_in(child);
}
}
visiting.remove(name);
memo.insert(name.to_string(), hazards);
hazards
}