diff --git a/aderyn_core/src/context/browser/external_calls.rs b/aderyn_core/src/context/browser/external_calls.rs new file mode 100644 index 00000000..e4ef0711 --- /dev/null +++ b/aderyn_core/src/context/browser/external_calls.rs @@ -0,0 +1,106 @@ +//! This module helps us detect whether a given AST Node has any external calls inside of it + +use super::ExtractMemberAccesses; +use crate::{ast::*, context::workspace_context::ASTNode}; + +fn is_external_call(ast_node: ASTNode) -> bool { + // This is so we can skip the FunctionCallOptions layer which solidity compiler inserts + // when there are options passed to function calls + for member_access in ExtractMemberAccesses::from(&ast_node).extracted { + // address(..).call("...") pattern + let is_call = member_access.member_name == "call"; + if is_call { + return true; + } + + // payable(address(..)).transfer(100) + // payable(address(..)).send(100) + // address.sendValue(..) (from openzeppelin) + if member_access.member_name == "transfer" + || member_access.member_name == "send" + || member_access.member_name == "sendValue" + { + if let Some(type_description) = member_access.expression.type_descriptions() { + if type_description + .type_string + .as_ref() + .is_some_and(|type_string| type_string.starts_with("address")) + { + return true; + } + } + } + + // Any external call + if member_access + .type_descriptions + .type_identifier + .is_some_and(|type_identifier| type_identifier.contains("function_external")) + { + return true; + } + } + + false +} + +impl FunctionCall { + pub fn is_external_call(&self) -> bool { + is_external_call(self.into()) + } +} +impl FunctionCallOptions { + pub fn is_external_call(&self) -> bool { + is_external_call(self.into()) + } +} + +#[cfg(test)] +mod external_calls_detector { + use crate::{ + context::browser::ExtractFunctionCalls, detect::test_utils::load_solidity_source_unit, + }; + + use super::FunctionDefinition; + + impl FunctionDefinition { + pub fn makes_external_calls(&self) -> bool { + let func_calls = ExtractFunctionCalls::from(self).extracted; + func_calls.iter().any(|f| f.is_external_call()) + } + } + + #[test] + fn test_direct_call_on_address() { + let context = + load_solidity_source_unit("../tests/contract-playground/src/ExternalCalls.sol"); + + let childex = context.find_contract_by_name("ChildEx"); + + let ext1 = childex.find_function_by_name("ext1"); + let ext2 = childex.find_function_by_name("ext2"); + let ext3 = childex.find_function_by_name("ext3"); + let ext4 = childex.find_function_by_name("ext4"); + let ext5 = childex.find_function_by_name("ext5"); + let ext6 = childex.find_function_by_name("ext6"); + let ext7 = childex.find_function_by_name("ext7"); + let ext8 = childex.find_function_by_name("ext8"); + let ext9 = childex.find_function_by_name("ext9"); + + assert!(ext1.makes_external_calls()); + assert!(ext2.makes_external_calls()); + assert!(ext3.makes_external_calls()); + assert!(ext4.makes_external_calls()); + assert!(ext5.makes_external_calls()); + assert!(ext6.makes_external_calls()); + assert!(ext7.makes_external_calls()); + assert!(ext8.makes_external_calls()); + assert!(ext9.makes_external_calls()); + + let notext1 = childex.find_function_by_name("notExt1"); + let notext2 = childex.find_function_by_name("notExt2"); + + assert!(!notext1.makes_external_calls()); + assert!(!notext2.makes_external_calls()); + } +} diff --git a/aderyn_core/src/context/browser/mod.rs b/aderyn_core/src/context/browser/mod.rs index e49ad838..327320d7 100644 --- a/aderyn_core/src/context/browser/mod.rs +++ b/aderyn_core/src/context/browser/mod.rs @@ -1,5 +1,6 @@ mod ancestral_line; mod closest_ancestor; +mod external_calls; mod extractor; mod immediate_children; mod location; @@ -13,6 +14,7 @@ mod sort_nodes; mod storage_vars; pub use ancestral_line::*; pub use closest_ancestor::*; +pub use external_calls::*; pub use extractor::*; pub use immediate_children::*; pub use location::*; diff --git a/tests/contract-playground/src/ExternalCalls.sol b/tests/contract-playground/src/ExternalCalls.sol new file mode 100644 index 00000000..78431b91 --- /dev/null +++ b/tests/contract-playground/src/ExternalCalls.sol @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +interface IMyTarget { + function extCall(uint256) external payable; +} + +contract MyTarget is IMyTarget { + uint256 s_a; + + function extCall(uint256 m_a) public payable { + s_a = m_a; + } +} + +contract BaseEx { + MyTarget t2; + + function baseThing(address x) public { + t2 = MyTarget(x); + } +} + +contract ChildEx is BaseEx { + MyTarget t1; + + constructor(MyTarget t) { + t1 = t; + } + + // Functions that make external calls + + function ext1() external payable { + t1.extCall(0); + } + + function ext2(address target) external { + MyTarget(target).extCall(0); + } + + function ext3() external { + this.ext1(); + } + + function ext4() external { + IMyTarget(address(t1)).extCall(0); + } + + // Functions that make external calls with options + + function ext5() external { + t1.extCall{gas: 100}(0); + } + + function ext6(address target) external { + MyTarget(target).extCall{value: 100}(0); + } + + function ext7() external { + this.ext1{gas: 100}(); + } + + function ext8() external { + IMyTarget(address(t1)).extCall{value: 100}(0); + } + + function ext9() external { + (bool success, ) = payable(address(t1)).call{ + value: address(this).balance + }(""); + if (success) { + revert(); + } + } + + // Functions that don't make external calls + + function notExt1() external { + super.baseThing(address(0)); + } + + function notExt2() external { + BaseEx.baseThing(address(0)); + } +}