Skip to content

Commit e912117

Browse files
TilakMaddyalexroan
andauthored
Re-entrancy detector + Control Flow Graph (#752)
Co-authored-by: Alex Roan <[email protected]>
1 parent 74fe8f8 commit e912117

36 files changed

+4925
-31
lines changed

.github/workflows/cargo.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ jobs:
6060
- name: Run tests
6161
run: |
6262
cargo build
63-
cargo test -- --nocapture
63+
cargo test -- --test-threads 1
6464
6565
lints:
6666
name: Lints

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,7 @@
22
.DS_Store
33
.venv
44
debug/
5+
dot/
6+
!dot/.gitkeep
57
# Generated by `oranda generate ci`
6-
public/
8+
public/

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ tokio = "1.40.0"
4444
toml = "0.8.13"
4545
tower-lsp = "0.20.0"
4646
dunce = "=1.0.4"
47+
petgraph = "0"
4748

4849
[profile.release]
4950
codegen-units = 1

aderyn_core/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ derive_more = { workspace = true }
2929
dunce = { workspace = true }
3030

3131
[dev-dependencies]
32+
petgraph = { workspace = true }
3233
cyfrin-foundry-compilers = { workspace = true, features = ["svm-solc"] }
3334
serial_test = { workspace = true }
3435
once_cell = { workspace = true }
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
//! This module helps us detect whether a given AST Node has any external calls inside of it
2+
3+
use super::ExtractMemberAccesses;
4+
use crate::{ast::*, context::workspace_context::ASTNode};
5+
6+
fn is_external_call(ast_node: ASTNode) -> bool {
7+
// This is so we can skip the FunctionCallOptions layer which solidity compiler inserts
8+
// when there are options passed to function calls
9+
for member_access in ExtractMemberAccesses::from(&ast_node).extracted {
10+
// address(..).call("...") pattern
11+
let is_call = member_access.member_name == "call";
12+
if is_call {
13+
return true;
14+
}
15+
16+
// payable(address(..)).transfer(100)
17+
// payable(address(..)).send(100)
18+
// address.sendValue(..) (from openzeppelin)
19+
if member_access.member_name == "transfer"
20+
|| member_access.member_name == "send"
21+
|| member_access.member_name == "sendValue"
22+
{
23+
if let Some(type_description) = member_access.expression.type_descriptions() {
24+
if type_description
25+
.type_string
26+
.as_ref()
27+
.is_some_and(|type_string| type_string.starts_with("address"))
28+
{
29+
return true;
30+
}
31+
}
32+
}
33+
34+
// Any external call
35+
if member_access
36+
.type_descriptions
37+
.type_identifier
38+
.is_some_and(|type_identifier| type_identifier.contains("function_external"))
39+
{
40+
return true;
41+
}
42+
}
43+
44+
false
45+
}
46+
47+
impl FunctionCall {
48+
pub fn is_external_call(&self) -> bool {
49+
is_external_call(self.into())
50+
}
51+
}
52+
impl FunctionCallOptions {
53+
pub fn is_external_call(&self) -> bool {
54+
is_external_call(self.into())
55+
}
56+
}
57+
58+
#[cfg(test)]
59+
mod external_calls_detector {
60+
use serial_test::serial;
61+
62+
use crate::{
63+
context::browser::ExtractFunctionCalls, detect::test_utils::load_solidity_source_unit,
64+
};
65+
66+
use super::FunctionDefinition;
67+
68+
impl FunctionDefinition {
69+
pub fn makes_external_calls(&self) -> bool {
70+
let func_calls = ExtractFunctionCalls::from(self).extracted;
71+
func_calls.iter().any(|f| f.is_external_call())
72+
}
73+
}
74+
75+
#[test]
76+
#[serial]
77+
fn test_direct_call_on_address() {
78+
let context =
79+
load_solidity_source_unit("../tests/contract-playground/src/ExternalCalls.sol");
80+
81+
let childex = context.find_contract_by_name("ChildEx");
82+
83+
let ext1 = childex.find_function_by_name("ext1");
84+
let ext2 = childex.find_function_by_name("ext2");
85+
let ext3 = childex.find_function_by_name("ext3");
86+
let ext4 = childex.find_function_by_name("ext4");
87+
let ext5 = childex.find_function_by_name("ext5");
88+
let ext6 = childex.find_function_by_name("ext6");
89+
let ext7 = childex.find_function_by_name("ext7");
90+
let ext8 = childex.find_function_by_name("ext8");
91+
let ext9 = childex.find_function_by_name("ext9");
92+
93+
assert!(ext1.makes_external_calls());
94+
assert!(ext2.makes_external_calls());
95+
assert!(ext3.makes_external_calls());
96+
assert!(ext4.makes_external_calls());
97+
assert!(ext5.makes_external_calls());
98+
assert!(ext6.makes_external_calls());
99+
assert!(ext7.makes_external_calls());
100+
assert!(ext8.makes_external_calls());
101+
assert!(ext9.makes_external_calls());
102+
103+
let notext1 = childex.find_function_by_name("notExt1");
104+
let notext2 = childex.find_function_by_name("notExt2");
105+
106+
assert!(!notext1.makes_external_calls());
107+
assert!(!notext2.makes_external_calls());
108+
}
109+
}

aderyn_core/src/context/browser/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
mod ancestral_line;
22
mod closest_ancestor;
3+
mod external_calls;
34
mod extractor;
45
mod immediate_children;
56
mod location;

0 commit comments

Comments
 (0)