Skip to content

Commit

Permalink
merge dev
Browse files Browse the repository at this point in the history
  • Loading branch information
alexroan committed Aug 2, 2024
2 parents d03d555 + c9c1bd1 commit f95d1a4
Show file tree
Hide file tree
Showing 19 changed files with 2,949 additions and 267 deletions.
452 changes: 240 additions & 212 deletions Cargo.lock

Large diffs are not rendered by default.

8 changes: 6 additions & 2 deletions aderyn_core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ license = "MIT"
crossbeam-channel = "0.5.9"
eyre = "0.6.12"
ignore = "0.4.21"
phf = {version = "0.11.2", features = ["macros"]}
phf = { version = "0.11.2", features = ["macros"] }
prettytable = "0.10.0"
rayon = "1.8.0"
semver = "1.0.20"
Expand All @@ -22,7 +22,11 @@ serde-sarif = "0.4.2"
serde_repr = "0.1.12"
strum = { version = "0.26", features = ["derive"] }
toml = "0.8.2"
cyfrin-foundry-compilers = { version = "0.3.20-aderyn", features = ["svm-solc"] }
cyfrin-foundry-compilers = { version = "0.3.20-aderyn", features = [
"svm-solc",
] }
num-bigint = "0.4"
num-traits = "0.2"
lazy-regex = "3.2.0"
derive_more = "0.99.18"

Expand Down
46 changes: 46 additions & 0 deletions aderyn_core/src/ast/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -146,3 +146,49 @@ impl From<Expression> for ASTNode {
}
}
}

impl From<Statement> for ASTNode {
fn from(value: Statement) -> Self {
match value {
Statement::Block(node) => node.into(),
Statement::Break(node) => node.into(),
Statement::Continue(node) => node.into(),
Statement::DoWhileStatement(node) => node.into(),
Statement::PlaceholderStatement(node) => node.into(),
Statement::VariableDeclarationStatement(node) => node.into(),
Statement::IfStatement(node) => node.into(),
Statement::ForStatement(node) => node.into(),
Statement::WhileStatement(node) => node.into(),
Statement::EmitStatement(node) => node.into(),
Statement::TryStatement(node) => node.into(),
Statement::UncheckedBlock(node) => node.into(),
Statement::Return(node) => node.into(),
Statement::RevertStatement(node) => node.into(),
Statement::ExpressionStatement(node) => node.into(),
Statement::InlineAssembly(node) => node.into(),
}
}
}

impl From<&Statement> for ASTNode {
fn from(value: &Statement) -> Self {
match value {
Statement::Block(node) => node.into(),
Statement::Break(node) => node.into(),
Statement::Continue(node) => node.into(),
Statement::DoWhileStatement(node) => node.into(),
Statement::PlaceholderStatement(node) => node.into(),
Statement::VariableDeclarationStatement(node) => node.into(),
Statement::IfStatement(node) => node.into(),
Statement::ForStatement(node) => node.into(),
Statement::WhileStatement(node) => node.into(),
Statement::EmitStatement(node) => node.into(),
Statement::TryStatement(node) => node.into(),
Statement::UncheckedBlock(node) => node.into(),
Statement::Return(node) => node.into(),
Statement::RevertStatement(node) => node.into(),
Statement::ExpressionStatement(node) => node.into(),
Statement::InlineAssembly(node) => node.into(),
}
}
}
8 changes: 8 additions & 0 deletions aderyn_core/src/context/investigator/callgraph_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ mod callgraph_tests {
},
};

use serial_test::serial;
use StandardInvestigationStyle::*;

fn get_function_by_name(context: &WorkspaceContext, name: &str) -> ASTNode {
Expand All @@ -35,6 +36,7 @@ mod callgraph_tests {
}

#[test]
#[serial]
fn test_callgraph_is_not_none() {
let context = crate::detect::test_utils::load_solidity_source_unit(
"../tests/contract-playground/src/CallGraphTests.sol",
Expand All @@ -44,6 +46,7 @@ mod callgraph_tests {
}

#[test]
#[serial]
fn test_tower1_modifier_has_no_downstream() {
let context = crate::detect::test_utils::load_solidity_source_unit(
"../tests/contract-playground/src/CallGraphTests.sol",
Expand All @@ -62,6 +65,7 @@ mod callgraph_tests {
}

#[test]
#[serial]
fn test_tower1_modifier_has_upstream() {
let context = crate::detect::test_utils::load_solidity_source_unit(
"../tests/contract-playground/src/CallGraphTests.sol",
Expand All @@ -80,6 +84,7 @@ mod callgraph_tests {
}

#[test]
#[serial]
fn test_tower2_modifier_has_both_upstream_and_downstream() {
let context = crate::detect::test_utils::load_solidity_source_unit(
"../tests/contract-playground/src/CallGraphTests.sol",
Expand All @@ -99,6 +104,7 @@ mod callgraph_tests {
}

#[test]
#[serial]
fn test_tower3_modifier_has_both_upstream_and_downstream() {
let context = crate::detect::test_utils::load_solidity_source_unit(
"../tests/contract-playground/src/CallGraphTests.sol",
Expand All @@ -120,6 +126,7 @@ mod callgraph_tests {
}

#[test]
#[serial]
fn test_tower3_functions_has_upstream() {
let context = crate::detect::test_utils::load_solidity_source_unit(
"../tests/contract-playground/src/CallGraphTests.sol",
Expand All @@ -137,6 +144,7 @@ mod callgraph_tests {
}

#[test]
#[serial]
fn test_tower4_functions_has_upstream_and_downstream() {
let context = crate::detect::test_utils::load_solidity_source_unit(
"../tests/contract-playground/src/CallGraphTests.sol",
Expand Down
13 changes: 13 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::<TautologyOrContraditionDetector>::default(),
Box::<DangerousStrictEqualityOnBalanceDetector>::default(),
Box::<StorageSignedIntegerArrayDetector>::default(),
Box::<RedundantStatementsDetector>::default(),
Expand All @@ -74,6 +75,8 @@ pub fn get_all_issue_detectors() -> Vec<Box<dyn IssueDetector>> {
Box::<PreDeclaredLocalVariableUsageDetector>::default(),
Box::<DeletionNestedMappingDetector>::default(),
Box::<HighLevelCallsInLoopDetector>::default(),
Box::<MsgValueUsedInLoopDetector>::default(),
Box::<ContractLocksEtherDetector>::default(),
]
}

Expand Down Expand Up @@ -136,6 +139,7 @@ pub(crate) enum IssueDetectorNamePool {
RTLO,
UncheckedReturn,
DangerousUnaryOperator,
TautologyOrContradiction,
DangerousStrictEquailtyOnContractBalance,
SignedStorageArray,
RedundantStatements,
Expand All @@ -144,6 +148,8 @@ pub(crate) enum IssueDetectorNamePool {
PreDeclaredLocalVariableUsage,
DeleteNestedMapping,
HighLevelCallsInLoop,
MsgValueInLoop,
ContractLocksEther,
// NOTE: `Undecided` will be the default name (for new bots).
// If it's accepted, a new variant will be added to this enum before normalizing it in aderyn
Undecided,
Expand Down Expand Up @@ -280,6 +286,9 @@ pub fn request_issue_detector_by_name(detector_name: &str) -> Option<Box<dyn Iss
IssueDetectorNamePool::DangerousUnaryOperator => {
Some(Box::<DangerousUnaryOperatorDetector>::default())
}
IssueDetectorNamePool::TautologyOrContradiction => {
Some(Box::<TautologyOrContraditionDetector>::default())
}
IssueDetectorNamePool::DangerousStrictEquailtyOnContractBalance => {
Some(Box::<DangerousStrictEqualityOnBalanceDetector>::default())
}
Expand All @@ -302,6 +311,10 @@ pub fn request_issue_detector_by_name(detector_name: &str) -> Option<Box<dyn Iss
IssueDetectorNamePool::HighLevelCallsInLoop => {
Some(Box::<HighLevelCallsInLoopDetector>::default())
}
IssueDetectorNamePool::MsgValueInLoop => Some(Box::<MsgValueUsedInLoopDetector>::default()),
IssueDetectorNamePool::ContractLocksEther => {
Some(Box::<ContractLocksEtherDetector>::default())
}
IssueDetectorNamePool::Undecided => None,
}
}
Expand Down
9 changes: 6 additions & 3 deletions aderyn_core/src/detect/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,9 @@ pub fn has_calls_that_sends_native_eth(ast_node: &ASTNode) -> bool {
if let Expression::Literal(literal) = c {
return literal.value.is_some();
}
if let Expression::Identifier(_) = c {
return true;
}
false
});
if !call_carries_value {
Expand Down Expand Up @@ -189,9 +192,9 @@ pub fn has_binary_checks_on_some_address(ast_node: &ASTNode) -> bool {
binary_operations.into_iter().any(|op| {
[op.left_expression, op.right_expression].iter().any(|op| {
op.as_ref().type_descriptions().is_some_and(|desc| {
desc.type_string
.as_ref()
.is_some_and(|type_string| type_string == "address")
desc.type_string.as_ref().is_some_and(|type_string| {
type_string == "address" || type_string == "address payable"
})
})
})
})
Expand Down
182 changes: 182 additions & 0 deletions aderyn_core/src/detect/high/contract_locks_ether.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
use std::collections::BTreeMap;

use std::convert::identity;
use std::error::Error;

use crate::ast::NodeID;

use crate::capture;
use crate::detect::detector::IssueDetectorNamePool;
use crate::{
context::workspace_context::WorkspaceContext,
detect::detector::{IssueDetector, IssueSeverity},
};

use eyre::Result;

#[derive(Default)]
pub struct ContractLocksEtherDetector {
// 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 ContractLocksEtherDetector {
fn detect(&mut self, context: &WorkspaceContext) -> Result<bool, Box<dyn Error>> {
for contract in context.contract_definitions() {
// If a contract can accept eth, but doesn't allow for withdrawal capture it!
if contract.can_accept_eth(context).is_some_and(identity)
&& !contract
.allows_withdrawal_of_eth(context)
.is_some_and(identity)
{
capture!(self, context, contract);
}
}
Ok(!self.found_instances.is_empty())
}

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

fn title(&self) -> String {
String::from("Contract locks Ether without a withdraw function.")
}

fn description(&self) -> String {
String::from(
"It appears that the contract includes a payable function to accept Ether but lacks a corresponding function to withdraw it, \
which leads to the Ether being locked in the contract. To resolve this issue, please implement a public or external function \
that allows for the withdrawal of Ether from the contract."
)
}

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

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

/// Handles tasks related to contract level analysis for eth
mod contract_eth_helper {
use crate::{
ast::{ASTNode, ContractDefinition, StateMutability, Visibility},
context::{
browser::ExtractFunctionDefinitions, investigator::*,
workspace_context::WorkspaceContext,
},
detect::helpers,
};

impl ContractDefinition {
pub fn can_accept_eth(&self, context: &WorkspaceContext) -> Option<bool> {
let contracts = self.linearized_base_contracts.as_ref()?;
for contract_id in contracts {
let funcs =
ExtractFunctionDefinitions::from(context.nodes.get(contract_id)?).extracted;
let num_payable_funcs = funcs
.into_iter()
.filter(|f| f.implemented && *f.state_mutability() == StateMutability::Payable)
.count();
if num_payable_funcs > 0 {
return Some(true);
}
}
Some(false)
}

pub fn allows_withdrawal_of_eth(&self, context: &WorkspaceContext) -> Option<bool> {
/*
For all the contracts in the hirearchy try and see if there is exists a public/external function that
can be called which takes the execution flow in a path where there is possibility to send back eth away from
the contract using the low level `call{value: XXX}` or `transfer` or `send`.
*/
let contracts = self.linearized_base_contracts.as_ref()?;
for contract_id in contracts {
if let ASTNode::ContractDefinition(contract) = context.nodes.get(contract_id)? {
let funcs = contract
.function_definitions()
.into_iter()
.filter(|f| {
f.implemented
&& (f.visibility == Visibility::Public
|| f.visibility == Visibility::External)
})
.map(|f| f.into())
.collect::<Vec<ASTNode>>();

let mut tracker = EthWithdrawalAllowerTracker::default();

let investigator = StandardInvestigator::new(
context,
funcs.iter().collect::<Vec<_>>().as_slice(),
StandardInvestigationStyle::Downstream,
)
.ok()?;

investigator.investigate(context, &mut tracker).ok()?;

if tracker.has_calls_that_sends_native_eth {
return Some(true);
}
}
}
// At this point we have successfully gone through all the contracts in the inheritance heirarchy
// but tracker has determined that none of them have have calls that sends native eth Even if they are by
// some chance, they are not reachable from external & public functions
Some(false)
}
}

#[derive(Default)]
struct EthWithdrawalAllowerTracker {
has_calls_that_sends_native_eth: bool,
}

impl StandardInvestigatorVisitor for EthWithdrawalAllowerTracker {
fn visit_any(&mut self, ast_node: &ASTNode) -> eyre::Result<()> {
if !self.has_calls_that_sends_native_eth
&& helpers::has_calls_that_sends_native_eth(ast_node)
{
self.has_calls_that_sends_native_eth = true;
}
Ok(())
}
}
}

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

use crate::detect::{
detector::IssueDetector, high::contract_locks_ether::ContractLocksEtherDetector,
};

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

let mut detector = ContractLocksEtherDetector::default();
let found = detector.detect(&context).unwrap();

println!("{:#?}", detector.instances());

// assert that the detector found an issue
assert!(found);
// assert that the detector found the correct number of instances
assert_eq!(detector.instances().len(), 2);
// assert the severity is high
assert_eq!(
detector.severity(),
crate::detect::detector::IssueSeverity::High
);
}
}
Loading

0 comments on commit f95d1a4

Please sign in to comment.