Skip to content

Commit

Permalink
Detector: Storage signed integer array (#624)
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 30, 2024
1 parent 6378621 commit 2aea4d0
Show file tree
Hide file tree
Showing 10 changed files with 398 additions and 43 deletions.
24 changes: 24 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion aderyn_core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,9 @@ 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"] }
lazy-regex = "3.2.0"
derive_more = "0.99.18"

[dev-dependencies]
serial_test = "3.0.0"
once_cell = "1.19.0"
once_cell = "1.19.0"
32 changes: 5 additions & 27 deletions aderyn_core/src/detect/detector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,33 +5,6 @@ use crate::{
ast::NodeID,
context::workspace_context::WorkspaceContext,
detect::{high::*, low::*},
detect::{
high::{
ArbitraryTransferFromDetector, AvoidAbiEncodePackedDetector,
BlockTimestampDeadlineDetector, DangerousUnaryOperatorDetector,
DelegateCallInLoopDetector, DynamicArrayLengthAssignmentDetector,
EnumerableLoopRemovalDetector, ExperimentalEncoderDetector,
IncorrectShiftOrderDetector, IncorrectUseOfCaretOperatorDetector,
MultipleConstructorsDetector, NestedStructInMappingDetector, RTLODetector,
ReusedContractNameDetector, SelfdestructIdentifierDetector,
StateVariableShadowingDetector, StorageArrayEditWithMemoryDetector,
TautologicalCompareDetector, UncheckedReturnDetector,
UninitializedStateVariableDetector, UnprotectedInitializerDetector,
UnsafeCastingDetector, YulReturnDetector,
},
low::{
CentralizationRiskDetector, ConstantsInsteadOfLiteralsDetector,
ContractsWithTodosDetector, DeprecatedOZFunctionsDetector,
DivisionBeforeMultiplicationDetector, EcrecoverDetector, EmptyBlockDetector,
InconsistentTypeNamesDetector, LargeLiteralValueDetector,
NonReentrantBeforeOthersDetector, PushZeroOpcodeDetector, RedundantStatementsDetector,
RequireWithStringDetector, RevertsAndRequiresInLoopsDetector,
SolmateSafeTransferLibDetector, UnindexedEventsDetector, UnsafeERC20FunctionsDetector,
UnsafeERC721MintDetector, UnspecificSolidityPragmaDetector, UselessErrorDetector,
UselessInternalFunctionDetector, UselessModifierDetector,
UselessPublicFunctionDetector, ZeroAddressCheckDetector,
},
},
};

use std::{
Expand Down Expand Up @@ -93,6 +66,7 @@ pub fn get_all_issue_detectors() -> Vec<Box<dyn IssueDetector>> {
Box::<RTLODetector>::default(),
Box::<UncheckedReturnDetector>::default(),
Box::<DangerousUnaryOperatorDetector>::default(),
Box::<StorageSignedIntegerArrayDetector>::default(),
Box::<RedundantStatementsDetector>::default(),
Box::<PublicVariableReadInExternalContextDetector>::default(),
Box::<WeakRandomnessDetector>::default(),
Expand Down Expand Up @@ -160,6 +134,7 @@ pub(crate) enum IssueDetectorNamePool {
RTLO,
UncheckedReturn,
DangerousUnaryOperator,
SignedStorageArray,
RedundantStatements,
PublicVariableReadInExternalContext,
WeakRandomness,
Expand Down Expand Up @@ -301,6 +276,9 @@ pub fn request_issue_detector_by_name(detector_name: &str) -> Option<Box<dyn Iss
IssueDetectorNamePool::DangerousUnaryOperator => {
Some(Box::<DangerousUnaryOperatorDetector>::default())
}
IssueDetectorNamePool::SignedStorageArray => {
Some(Box::<StorageSignedIntegerArrayDetector>::default())
}
IssueDetectorNamePool::RedundantStatements => {
Some(Box::<RedundantStatementsDetector>::default())
}
Expand Down
2 changes: 2 additions & 0 deletions aderyn_core/src/detect/high/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ pub(crate) mod selfdestruct;
pub(crate) mod send_ether_no_checks;
pub(crate) mod state_variable_shadowing;
pub(crate) mod storage_array_edit_with_memory;
pub(crate) mod storage_signed_integer_array;
pub(crate) mod tautological_compare;
pub(crate) mod unchecked_return;
pub(crate) mod unchecked_send;
Expand Down Expand Up @@ -51,6 +52,7 @@ pub use selfdestruct::SelfdestructIdentifierDetector;
pub use send_ether_no_checks::SendEtherNoChecksDetector;
pub use state_variable_shadowing::StateVariableShadowingDetector;
pub use storage_array_edit_with_memory::StorageArrayEditWithMemoryDetector;
pub use storage_signed_integer_array::StorageSignedIntegerArrayDetector;
pub use tautological_compare::TautologicalCompareDetector;
pub use unchecked_return::UncheckedReturnDetector;
pub use unchecked_send::UncheckedSendDetector;
Expand Down
206 changes: 206 additions & 0 deletions aderyn_core/src/detect/high/storage_signed_integer_array.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
use std::collections::BTreeMap;
use std::error::Error;
use std::str::FromStr;

use crate::ast::{
ASTNode, Expression, Identifier, NodeID, TupleExpression, TypeDescriptions, UnaryOperation,
};

use crate::capture;
use crate::context::browser::{
ExtractPragmaDirectives, ExtractTupleExpressions, GetImmediateParent,
};
use crate::detect::detector::IssueDetectorNamePool;
use crate::detect::helpers;
use crate::{
context::workspace_context::WorkspaceContext,
detect::detector::{IssueDetector, IssueSeverity},
};
use eyre::Result;
use lazy_regex::regex;
use semver::{Version, VersionReq};

#[derive(Default)]
pub struct StorageSignedIntegerArrayDetector {
// 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 StorageSignedIntegerArrayDetector {
fn detect(&mut self, context: &WorkspaceContext) -> Result<bool, Box<dyn Error>> {
for source_unit in context.source_units() {
let tuple_expressions = ExtractTupleExpressions::from(source_unit).extracted;
let pragma_directives = ExtractPragmaDirectives::from(source_unit).extracted;

if let Some(pragma_directive) = pragma_directives.first() {
if let Ok(pragma_semver) = helpers::pragma_directive_to_semver(pragma_directive) {
if version_req_allows_below_0_5_10(&pragma_semver) {
// Search for a literal array with one negative value in it
for tuple_expression in tuple_expressions
.into_iter()
.filter(|tuple_expression| tuple_expression.is_inline_array)
{
// First, make sure it's being assigned to an array pointer to storage
if !is_tuple_being_assigned_to_storage_array(&tuple_expression, context)
{
continue;
}

// Now, make sure there is at least 1 negative value in the tuple array
let negative_component_present =
tuple_expression.components.iter().any(|c| {
if let Some(Expression::UnaryOperation(UnaryOperation {
operator,
..
})) = c
{
return operator == "-";
}
false
});

if negative_component_present {
capture!(self, context, tuple_expression);
}
}
}
}
}
}

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

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

fn title(&self) -> String {
String::from(
"Compiler Bug: Signed array in storage detected for compiler version `<0.5.10`",
)
}

fn description(&self) -> String {
String::from("If you want to leverage signed arrays in storage by assigning a literal array with at least one negative \
number, then you mus use solidity version 0.5.10 or above. This is because of a bug in older compilers.")
}

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

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

fn version_req_allows_below_0_5_10(version_req: &VersionReq) -> bool {
// If it matches any 0.4.0 to 0.4.26, return true
for i in 0..=26 {
let version = Version::from_str(&format!("0.4.{}", i)).unwrap();
if version_req.matches(&version) {
return true;
}
}

// If it matches any 0.5.0 to 0.5.9 return true
for i in 0..=9 {
let version = Version::from_str(&format!("0.5.{}", i)).unwrap();
if version_req.matches(&version) {
return true;
}
}

// Else, return false
false
}

// Build a regular expression to catch type names that correspond to pointers to storage arrays
static SIGNED_STORAGE_ARRAY_POINTER: &lazy_regex::Lazy<lazy_regex::Regex> =
regex!(r"^int[0-9]*\[[0-9]*] storage ref$");

fn is_tuple_being_assigned_to_storage_array(
tuple_expression: &TupleExpression,
context: &WorkspaceContext,
) -> bool {
if let Some(ASTNode::Assignment(assignment)) = tuple_expression.parent(context) {
if let Expression::Identifier(Identifier {
type_descriptions:
TypeDescriptions {
type_string: Some(type_string),
..
},
..
}) = assignment.left_hand_side.as_ref()
{
if SIGNED_STORAGE_ARRAY_POINTER.is_match(type_string) {
return true;
}
}
}
false
}

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

use crate::detect::{
detector::IssueDetector,
high::storage_signed_integer_array::{
StorageSignedIntegerArrayDetector, SIGNED_STORAGE_ARRAY_POINTER,
},
};

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

let mut detector = StorageSignedIntegerArrayDetector::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(), 1);
// assert the severity is high
assert_eq!(
detector.severity(),
crate::detect::detector::IssueSeverity::High
);
// assert the title is correct
assert_eq!(
detector.title(),
String::from(
"Compiler Bug: Signed array in storage detected for compiler version `<0.5.10`",
)
);
// assert the description is correct
assert_eq!(
detector.description(),
String::from("If you want to leverage signed arrays in storage by assigning a literal array with at least one negative \
number, then you mus use solidity version 0.5.10 or above. This is because of a bug in older compilers.")
);
}

#[test]
fn test_regular_expression_works() {
// TARGET signed storage array references

assert!(SIGNED_STORAGE_ARRAY_POINTER.is_match("int256[3] storage ref"));
assert!(SIGNED_STORAGE_ARRAY_POINTER.is_match("int[1300] storage ref"));
assert!(SIGNED_STORAGE_ARRAY_POINTER.is_match("int8[] storage ref"));
assert!(SIGNED_STORAGE_ARRAY_POINTER.is_match("int[] storage ref"));
assert!(!SIGNED_STORAGE_ARRAY_POINTER.is_match("uint256[3] storage ref"));
assert!(!SIGNED_STORAGE_ARRAY_POINTER.is_match("uint[1300] storage ref"));
assert!(!SIGNED_STORAGE_ARRAY_POINTER.is_match("uint8[] storage ref"));
assert!(!SIGNED_STORAGE_ARRAY_POINTER.is_match("uint[] storage ref"));
}
}
1 change: 1 addition & 0 deletions reports/adhoc-sol-files-highs-only-report.json
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@
"rtlo",
"unchecked-return",
"dangerous-unary-operator",
"signed-storage-array",
"weak-randomness",
"pre-declared-local-variable-usage",
"delete-nested-mapping"
Expand Down
Loading

0 comments on commit 2aea4d0

Please sign in to comment.