Skip to content

Commit

Permalink
Detector: Public variable read in an external context (#619)
Browse files Browse the repository at this point in the history
Co-authored-by: Alex Roan <alex@cyfrin.io>
  • Loading branch information
TilakMaddy and alexroan authored Jul 29, 2024
1 parent 5273e5c commit 25a368c
Show file tree
Hide file tree
Showing 7 changed files with 384 additions and 8 deletions.
5 changes: 5 additions & 0 deletions aderyn_core/src/detect/detector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ pub fn get_all_issue_detectors() -> Vec<Box<dyn IssueDetector>> {
Box::<RTLODetector>::default(),
Box::<UncheckedReturnDetector>::default(),
Box::<DangerousUnaryOperatorDetector>::default(),
Box::<PublicVariableReadInExternalContextDetector>::default(),
Box::<WeakRandomnessDetector>::default(),
Box::<PreDeclaredLocalVariableUsageDetector>::default(),
Box::<DeletionNestedMappingDetector>::default(),
Expand Down Expand Up @@ -131,6 +132,7 @@ pub(crate) enum IssueDetectorNamePool {
RTLO,
UncheckedReturn,
DangerousUnaryOperator,
PublicVariableReadInExternalContext,
WeakRandomness,
PreDeclaredLocalVariableUsage,
DeleteNestedMapping,
Expand Down Expand Up @@ -270,6 +272,9 @@ pub fn request_issue_detector_by_name(detector_name: &str) -> Option<Box<dyn Iss
IssueDetectorNamePool::DangerousUnaryOperator => {
Some(Box::<DangerousUnaryOperatorDetector>::default())
}
IssueDetectorNamePool::PublicVariableReadInExternalContext => {
Some(Box::<PublicVariableReadInExternalContextDetector>::default())
}
IssueDetectorNamePool::WeakRandomness => Some(Box::<WeakRandomnessDetector>::default()),
IssueDetectorNamePool::PreDeclaredLocalVariableUsage => {
Some(Box::<PreDeclaredLocalVariableUsageDetector>::default())
Expand Down
2 changes: 2 additions & 0 deletions aderyn_core/src/detect/low/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ pub(crate) mod empty_blocks;
pub(crate) mod inconsistent_type_names;
pub(crate) mod large_literal_value;
pub(crate) mod non_reentrant_before_others;
pub(crate) mod public_variable_read_in_external_context;
pub(crate) mod push_0_opcode;
pub(crate) mod require_with_string;
pub(crate) mod reverts_and_requries_in_loops;
Expand All @@ -32,6 +33,7 @@ pub use empty_blocks::EmptyBlockDetector;
pub use inconsistent_type_names::InconsistentTypeNamesDetector;
pub use large_literal_value::LargeLiteralValueDetector;
pub use non_reentrant_before_others::NonReentrantBeforeOthersDetector;
pub use public_variable_read_in_external_context::PublicVariableReadInExternalContextDetector;
pub use push_0_opcode::PushZeroOpcodeDetector;
pub use require_with_string::RequireWithStringDetector;
pub use reverts_and_requries_in_loops::RevertsAndRequiresInLoopsDetector;
Expand Down
170 changes: 170 additions & 0 deletions aderyn_core/src/detect/low/public_variable_read_in_external_context.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
use std::collections::{BTreeMap, HashSet};
use std::error::Error;

use crate::ast::{
ASTNode, ContractDefinition, Expression, Identifier, MemberAccess, NodeID, Visibility,
};

use crate::capture;
use crate::context::browser::{ExtractFunctionCalls, ExtractVariableDeclarations};
use crate::detect::detector::IssueDetectorNamePool;
use crate::{
context::workspace_context::WorkspaceContext,
detect::detector::{IssueDetector, IssueSeverity},
};
use eyre::{eyre, Result};

#[derive(Default)]
pub struct PublicVariableReadInExternalContextDetector {
// Keys are: [0] source file name, [1] line number, [2] character location of node.
// Do not add items manually, use `capture!` to add nodes to this BTreeMap.
found_instances: BTreeMap<(String, usize, String), NodeID>,
}

impl IssueDetector for PublicVariableReadInExternalContextDetector {
fn detect(&mut self, context: &WorkspaceContext) -> Result<bool, Box<dyn Error>> {
for contract in context.contract_definitions() {
// Public state variables including the base contracts
if let Ok(public_state_variable_names) =
find_all_public_state_variables_names_for_contract(context, contract)
{
// Find all the `X`s that appear with the pattern `this.X()`
let this_member_accesses =
find_all_public_member_names_called_using_this_keyword_in_contract(
context, contract,
);

for member_access in this_member_accesses {
if public_state_variable_names.contains(&member_access.member_name) {
capture!(self, context, member_access);
}
}
}
}

Ok(!self.found_instances.is_empty())
}

fn severity(&self) -> IssueSeverity {
IssueSeverity::Low
}

fn title(&self) -> String {
String::from("Public variables of a contract read in an external context (using `this`).")
}

fn description(&self) -> String {
String::from(
"The contract reads it's own variable using `this` which adds an unnecessary STATICCALL. Remove `this` and access the variable like storage.",
)
}

fn instances(&self) -> BTreeMap<(String, usize, String), NodeID> {
self.found_instances.clone()
}

fn name(&self) -> String {
IssueDetectorNamePool::PublicVariableReadInExternalContext.to_string()
}
}

fn find_all_public_member_names_called_using_this_keyword_in_contract<'a>(
context: &'a WorkspaceContext,
contract: &ContractDefinition,
) -> Vec<&'a MemberAccess> {
let mut member_names = vec![];

let function_calls = ExtractFunctionCalls::from(contract).extracted;
for function_call in function_calls {
if let Expression::MemberAccess(MemberAccess { id, expression, .. }) =
function_call.expression.as_ref()
{
if let Expression::Identifier(Identifier { name, .. }) = expression.as_ref() {
if name == "this" {
if let Some(ASTNode::MemberAccess(member_acess)) = context.nodes.get(id) {
member_names.push(member_acess)
}
}
}
}
}

member_names
}

// Scans the linearized base contracts and returns a list of all the NodeIDs of public variable declarations
fn find_all_public_state_variables_names_for_contract(
context: &WorkspaceContext,
contract: &ContractDefinition,
) -> Result<HashSet<String>, Box<dyn Error>> {
let inheritance_ancestors = contract
.linearized_base_contracts
.as_ref()
.ok_or(eyre!("base contracts not found!"))?;

Ok(inheritance_ancestors
.iter()
.flat_map(|ancestor_id| {
if let Some(ancestor) = context.nodes.get(ancestor_id) {
let public_variable_declaraions =
ExtractVariableDeclarations::from(ancestor).extracted;
return Some(
public_variable_declaraions
.into_iter()
.filter(|declaration| {
declaration.state_variable
&& declaration.visibility == Visibility::Public
})
.collect::<Vec<_>>(),
);
}
None
})
.flatten()
.map(|v| v.name.clone())
.collect())
}

#[cfg(test)]
mod public_variable_read_in_external_context_detector_tests {
use serial_test::serial;

use crate::detect::{
detector::IssueDetector,
low::public_variable_read_in_external_context::PublicVariableReadInExternalContextDetector,
};

#[test]
#[serial]
fn test_public_variable_read_in_external_context() {
let context = crate::detect::test_utils::load_solidity_source_unit(
"../tests/contract-playground/src/PublicVariableReadInExternalContext.sol",
);

let mut detector = PublicVariableReadInExternalContextDetector::default();
let found = detector.detect(&context).unwrap();
// assert that the detector found an issue
assert!(found);
// assert that the detector found the correct number of instances
assert_eq!(detector.instances().len(), 4);
// assert the severity is low
assert_eq!(
detector.severity(),
crate::detect::detector::IssueSeverity::Low
);
// assert the title is correct
assert_eq!(
detector.title(),
String::from(
"Public variables of a contract read in an external context (using `this`)."
)
);
// assert the description is correct
assert_eq!(
detector.description(),
String::from(
"The contract reads it's own variable using `this` which adds an unnecessary STATICCALL. Remove `this` and access the variable like storage.",
)
);
}
}
48 changes: 45 additions & 3 deletions reports/report.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"files_summary": {
"total_source_units": 68,
"total_sloc": 1922
"total_source_units": 69,
"total_sloc": 1954
},
"files_details": {
"files_details": [
Expand Down Expand Up @@ -121,6 +121,10 @@
"file_path": "src/PreDeclaredVarUsage.sol",
"n_sloc": 9
},
{
"file_path": "src/PublicVariableReadInExternalContext.sol",
"n_sloc": 32
},
{
"file_path": "src/RTLO.sol",
"n_sloc": 7
Expand Down Expand Up @@ -281,7 +285,7 @@
},
"issue_count": {
"high": 30,
"low": 23
"low": 24
},
"high_issues": {
"issues": [
Expand Down Expand Up @@ -1271,6 +1275,12 @@
"src": "355:7",
"src_char": "355:7"
},
{
"contract_path": "src/PublicVariableReadInExternalContext.sol",
"line_no": 6,
"src": "130:26",
"src_char": "130:26"
},
{
"contract_path": "src/StateShadowing.sol",
"line_no": 5,
Expand Down Expand Up @@ -3479,6 +3489,37 @@
"src_char": "541:5"
}
]
},
{
"title": "Public variables of a contract read in an external context (using `this`).",
"description": "The contract reads it's own variable using `this` which adds an unnecessary STATICCALL. Remove `this` and access the variable like storage.",
"detector_name": "public-variable-read-in-external-context",
"instances": [
{
"contract_path": "src/PublicVariableReadInExternalContext.sol",
"line_no": 12,
"src": "355:14",
"src_char": "355:14"
},
{
"contract_path": "src/PublicVariableReadInExternalContext.sol",
"line_no": 16,
"src": "457:16",
"src_char": "457:16"
},
{
"contract_path": "src/PublicVariableReadInExternalContext.sol",
"line_no": 20,
"src": "553:12",
"src_char": "553:12"
},
{
"contract_path": "src/PublicVariableReadInExternalContext.sol",
"line_no": 42,
"src": "1175:14",
"src_char": "1175:14"
}
]
}
]
},
Expand Down Expand Up @@ -3533,6 +3574,7 @@
"rtlo",
"unchecked-return",
"dangerous-unary-operator",
"public-variable-read-in-external-context",
"weak-randomness",
"pre-declared-local-variable-usage",
"delete-nested-mapping"
Expand Down
Loading

0 comments on commit 25a368c

Please sign in to comment.