diff --git a/slither/detectors/all_detectors.py b/slither/detectors/all_detectors.py index ff1c352c3..e22f6cca3 100644 --- a/slither/detectors/all_detectors.py +++ b/slither/detectors/all_detectors.py @@ -99,3 +99,6 @@ from .statements.return_bomb import ReturnBomb from .functions.out_of_order_retryable import OutOfOrderRetryable from .statements.unused_import import UnusedImport +from .oracles.oracle_data_validation import OracleDataCheck +from .oracles.oracle_sequencer import SequencerCheck +from .oracles.deprecated_chainlink_calls import DeprecatedChainlinkCall diff --git a/slither/detectors/oracles/__init__.py b/slither/detectors/oracles/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/slither/detectors/oracles/deprecated_chainlink_calls.py b/slither/detectors/oracles/deprecated_chainlink_calls.py new file mode 100644 index 000000000..4a37970a0 --- /dev/null +++ b/slither/detectors/oracles/deprecated_chainlink_calls.py @@ -0,0 +1,62 @@ +from slither.core.declarations.contract import Contract +from slither.detectors.abstract_detector import AbstractDetector, DetectorClassification +from slither.slithir.operations import HighLevelCall + + +class DeprecatedChainlinkCall(AbstractDetector): + """ + Documentation: This detector scans for deprecated Chainlink API calls in Solidity contracts. For example, it flags the use of `getAnswer` which is no longer recommended. + """ + + ARGUMENT = "deprecated-chainlink-call" + HELP = "Oracle vulnerabilities" + IMPACT = DetectorClassification.MEDIUM + CONFIDENCE = DetectorClassification.MEDIUM + + WIKI = "https://github.com/crytic/slither/wiki/Detector-Documentation#deprecated-chainlink-call" + WIKI_TITLE = "Deprecated Chainlink call" + WIKI_DESCRIPTION = "Detection of deprecated Chainlink call." + WIKI_RECOMMENDATION = "Do not use deprecated Chainlink calls. Visit https://docs.chain.link/data-feeds/api-reference/ for active API calls." + WIKI_EXPLOIT_SCENARIO = "---" + + DEPRECATED_CHAINLINK_CALLS = [ + "getAnswer", + "getTimestamp", + "latestAnswer", + "latestRound", + "latestTimestamp", + ] + + def is_old_chainlink_call(self, ir) -> bool: + """ + Check if the given operation is an old Chainlink call. + """ + if isinstance(ir, HighLevelCall): + if ( + ir.function.name in self.DEPRECATED_CHAINLINK_CALLS + and str(ir.destination.type) == "AggregatorV3Interface" + ): + return True + return False + + def find_usage_of_deprecated_chainlink_calls(self, contracts: Contract): + """ + Find usage of deprecated Chainlink calls in the contracts. + """ + results = [] + for contract in contracts: + for function in contract.functions: + for node in function.nodes: + for ir in node.irs: + if self.is_old_chainlink_call(ir): + results.append( + f"Deprecated Chainlink call {ir.destination}.{ir.function.name} used ({node.source_mapping}).\n" + ) + return results + + def _detect(self): + results = self.find_usage_of_deprecated_chainlink_calls(self.contracts) + if len(results) > 0: + res = self.generate_result(results) + return [res] + return [] diff --git a/slither/detectors/oracles/oracle_data_validation.py b/slither/detectors/oracles/oracle_data_validation.py new file mode 100644 index 000000000..b9b94c21b --- /dev/null +++ b/slither/detectors/oracles/oracle_data_validation.py @@ -0,0 +1,44 @@ +from slither.detectors.abstract_detector import DetectorClassification +from slither.detectors.oracles.oracle_detector import OracleDetector + + +class OracleDataCheck(OracleDetector): + """ + Documentation + """ + + ARGUMENT = "oracle-data-validation" # slither will launch the detector with slither.py --detect mydetector + HELP = "Oracle vulnerabilities" + IMPACT = DetectorClassification.MEDIUM + CONFIDENCE = DetectorClassification.MEDIUM + + WIKI = "https://github.com/crytic/slither/wiki/Detector-Documentation#oracle-data-validation" + + WIKI_TITLE = "Oracle data validation" + WIKI_DESCRIPTION = "The detection of not correct validation of oracle data." + WIKI_EXPLOIT_SCENARIO = "---" + WIKI_RECOMMENDATION = "Validate the data returned by the oracle. For more information visit https://docs.chain.link/data-feeds/api-reference" + + # This function is necessary even though there is a detector for unused return values because the variable can be used but will not be validated in conditional statements + def process_not_checked_vars(self): + result = [] + for oracle in self.oracles: + if len(oracle.vars_not_in_condition) > 0: + result.append( + f"The oracle {oracle.contract}.{oracle.interface} ({oracle.node.source_mapping}) returns the variables {[var.name for var in oracle.vars_not_in_condition]} which are not validated. It can potentially lead to unexpected behaviour.\n" + ) + return result + + def _detect(self): + results = [] + super()._detect() + not_checked_vars = self.process_not_checked_vars() + if len(not_checked_vars) > 0: + res = self.generate_result(not_checked_vars) + results.append(res) + for oracle in self.oracles: + checked_vars = oracle.naive_data_validation() + if len(checked_vars) > 0: + res = self.generate_result(checked_vars) + results.append(res) + return results diff --git a/slither/detectors/oracles/oracle_detector.py b/slither/detectors/oracles/oracle_detector.py new file mode 100644 index 000000000..8073ee58c --- /dev/null +++ b/slither/detectors/oracles/oracle_detector.py @@ -0,0 +1,288 @@ +from typing import List +from slither.analyses.data_dependency.data_dependency import get_dependencies +from slither.core.declarations.contract import Contract +from slither.core.declarations.function_contract import FunctionContract +from slither.core.variables.state_variable import StateVariable +from slither.detectors.abstract_detector import AbstractDetector +from slither.slithir.operations import HighLevelCall, InternalCall, Operation, Unpack +from slither.slithir.variables import TupleVariable +from slither.detectors.oracles.supported_oracles.oracle import Oracle, VarInCondition +from slither.detectors.oracles.supported_oracles.chainlink_oracle import ChainlinkOracle +from slither.detectors.oracles.supported_oracles.help_functions import is_internal_call +from slither.analyses.data_dependency.data_dependency import is_dependent +from slither.core.expressions.tuple_expression import TupleExpression + + +class OracleDetector(AbstractDetector): + def __init__(self, compilation_unit, slither, logger): + super().__init__(compilation_unit, slither, logger) + self.oracles = [] + self.nodes_with_var = [] + + # If the node is high level call, return the interface and the function name + @staticmethod + def obtain_interface_and_api(node) -> (str, str): + for ir in node.irs: + if isinstance(ir, HighLevelCall): + return ir.destination, ir.function.name + return None, None + + @staticmethod + def generate_oracle(ir: Operation) -> Oracle: + if ChainlinkOracle().is_instance_of(ir): + return ChainlinkOracle() + return None + + @staticmethod + def get_returned_variables_from_oracle(node) -> list: + written_vars = [] + ordered_vars = [] + for var in node.variables_written: + written_vars.append(var) + for exp in node.variables_written_as_expression: + if isinstance(exp, TupleExpression): + for v in exp.expressions: + for var in written_vars: + if str(v) == str(var.name): + ordered_vars.append(var) + if len(ordered_vars) == 0: + ordered_vars = written_vars + return ordered_vars + + @staticmethod + def check_var_condition_match(var, node) -> bool: + for ( + var2 + ) in ( + node.variables_read + ): # This iterates through all variables which are read in node, what means that they are used in condition + if var is None or var2 is None: + return False + if var.name == var2.name: + return True + return False + + @staticmethod + def map_param_to_var(var, function: FunctionContract): + for param in function.parameters: + try: + origin_vars = get_dependencies(param, function) + # DATA_DEPENDENCY error can be throw out + except KeyError: + continue + for var2 in origin_vars: + if var2 == var: + return param + return None + + @staticmethod + def match_index_to_node(indexes, node): + idxs = [] + for idx in indexes: + if idx[0] == node: + idxs.append(idx[1]) + return idxs + + def find_oracles(self, contracts: Contract) -> List[Oracle]: + """ + Detects off-chain oracle contract and VAR + """ + oracles = [] + for contract in contracts: + for function in contract.functions: + if function.is_constructor: + continue + ( + returned_oracles, + oracle_returned_var_indexes, + ) = self.oracle_call(function) + if returned_oracles: + for oracle in returned_oracles: + (interface, oracle_api) = self.obtain_interface_and_api(oracle.node) + idxs = self.match_index_to_node(oracle_returned_var_indexes, oracle.node) + oracle.set_data(contract, function, idxs, interface, oracle_api) + oracles.append(oracle) + return oracles + + # This function was inspired by detector unused return values + def oracle_call(self, function: FunctionContract): + used_returned_vars = [] + values_returned = [] + nodes_origin = {} + oracles = [] + for node in function.nodes: # pylint: disable=too-many-nested-blocks + for ir in node.irs: + oracle = self.generate_oracle(ir) + if oracle: + oracle.set_node(node) + oracles.append(oracle) + if ir.lvalue and not isinstance(ir.lvalue, StateVariable): + values_returned.append((ir.lvalue, None)) + nodes_origin[ir.lvalue] = ir + if isinstance(ir.lvalue, TupleVariable): + # we iterate the number of elements the tuple has + # and add a (variable, index) in values_returned for each of them + for index in range(len(ir.lvalue.type)): + values_returned.append((ir.lvalue, index)) + for read in ir.read: + remove = (read, ir.index) if isinstance(ir, Unpack) else (read, None) + if remove in values_returned: + used_returned_vars.append( + remove + ) # This is saying which element is used based on the index + # this is needed to remove the tuple variable when the first time one of its element is used + if remove[1] is not None and (remove[0], None) in values_returned: + values_returned.remove((remove[0], None)) + values_returned.remove(remove) + returned_vars_used_indexes = [] + for (value, index) in used_returned_vars: + returned_vars_used_indexes.append((nodes_origin[value].node, index)) + return oracles, returned_vars_used_indexes + + def map_condition_to_var(self, var, function: FunctionContract): + nodes = [] + for node in function.nodes: + if node.is_conditional() and self.check_var_condition_match(var, node): + nodes.append(node) + return nodes + + # Check if the vars occurs in require/assert statement or in conditional node + def vars_in_conditions(self, oracle: Oracle): + vars_not_in_condition = [] + oracle_vars = [] + nodes = [] + for var in oracle.oracle_vars: + self.nodes_with_var = [] + if oracle.function.is_reading_in_conditional_node( + var + ) or oracle.function.is_reading_in_require_or_assert(var): + self.nodes_with_var = self.map_condition_to_var(var, oracle.function) + for node in self.nodes_with_var: + for ir in node.irs: + if isinstance(ir, InternalCall): + self.investigate_internal_call( + oracle, + ir.function, + var, + ) + oracle.remove_occurances_in_function() + + if len(self.nodes_with_var) > 0: + oracle_vars.append(VarInCondition(var, self.nodes_with_var)) + else: + if self.investigate_internal_call(oracle, oracle.function, var): + oracle_vars.append(VarInCondition(var, self.nodes_with_var)) + oracle.remove_occurances_in_function() + elif nodes := self.investigate_on_return(oracle, var): + oracle_vars.append(VarInCondition(var, nodes)) + oracle.out_of_function_checks.append((var, nodes)) + oracle.remove_occurances_in_function() + else: + vars_not_in_condition.append(var) + oracle_vars.append(var) + + oracle.vars_not_in_condition = vars_not_in_condition + oracle.oracle_vars = oracle_vars + return True + + def checks_performed_out_of_original_function(self, oracle, returned_var): + nodes_of_call = [] + functions_of_call = [] + original_function = oracle.function + original_node = oracle.node + for contract in self.contracts: + for function in contract.functions: + if function == oracle.function: + continue + nodes, functions = self.find_if_original_function_called(oracle, function) + if nodes and functions: + nodes_of_call.extend(nodes) + functions_of_call.extend(functions) + if not nodes_of_call or not functions_of_call: + return [] + + i = 0 + nodes = [] + for node in nodes_of_call: + oracle.set_function(functions_of_call[i]) + if not oracle.add_occurance_in_function(functions_of_call[i]): + break + oracle.set_node(node) + new_vars = self.get_returned_variables_from_oracle(node) + for var in new_vars: + if is_dependent(var, returned_var, node): + oracle.oracle_vars = [var] + break + self.vars_in_conditions(oracle) + if isinstance(oracle.oracle_vars[0], VarInCondition): + nodes.extend(oracle.oracle_vars[0].nodes_with_var) + i += 1 + + # Return back original node and function after recursion to let developer know on which line the oracle is used + oracle.set_function(original_function) + oracle.set_node(original_node) + return nodes + + @staticmethod + def find_if_original_function_called(oracle, function): + nodes_of_call = [] + functions_of_call = [] + for node in function.nodes: + for ir in node.irs: + if isinstance(ir, (InternalCall, HighLevelCall)): + if ir.function == oracle.function: + nodes_of_call.append(node) + functions_of_call.append(function) + if ir.function == function: + return None, None + return nodes_of_call, functions_of_call + + def investigate_on_return(self, oracle, var) -> bool: + for value in oracle.function.return_values: + if is_dependent(value, var, oracle.node): + return self.checks_performed_out_of_original_function(oracle, value) + + return False + + # This function interates through all internal calls in function and checks if the var is used in condition any of them + def investigate_internal_call(self, oracle, function: FunctionContract, var) -> bool: + if function is None: + return False + + result = oracle.add_occurance_in_function(function) + if not result: + return False + + original_var_as_param = self.map_param_to_var(var, function) + if original_var_as_param is None: + original_var_as_param = var + + if function.is_reading_in_conditional_node( + original_var_as_param + ) or function.is_reading_in_require_or_assert(original_var_as_param): + conditions = [] + for node in function.nodes: + if ( + node.is_conditional() + and self.check_var_condition_match(original_var_as_param, node) + and not is_internal_call(node) + ): + conditions.append(node) + if len(conditions) > 0: + for cond in conditions: + self.nodes_with_var.append(cond) + return True + + for node in function.nodes: + for ir in node.irs: + if isinstance(ir, InternalCall): + if self.investigate_internal_call(oracle, ir.function, original_var_as_param): + return True + + return False + + def _detect(self): + self.oracles = self.find_oracles(self.contracts) + for oracle in self.oracles: + oracle.oracle_vars = self.get_returned_variables_from_oracle(oracle.node) + self.vars_in_conditions(oracle) diff --git a/slither/detectors/oracles/oracle_sequencer.py b/slither/detectors/oracles/oracle_sequencer.py new file mode 100644 index 000000000..d7067d137 --- /dev/null +++ b/slither/detectors/oracles/oracle_sequencer.py @@ -0,0 +1,35 @@ +from slither.detectors.abstract_detector import DetectorClassification +from slither.detectors.oracles.oracle_detector import OracleDetector + + +class SequencerCheck(OracleDetector): + + """ + Documentation + """ + + ARGUMENT = ( + "oracle-sequencer" # slither will launch the detector with slither.py --detect mydetector + ) + HELP = "Oracle vulnerabilities" + IMPACT = DetectorClassification.INFORMATIONAL + CONFIDENCE = DetectorClassification.INFORMATIONAL + + WIKI = "https://github.com/crytic/slither/wiki/Detector-Documentation#oracle-sequencer" + + WIKI_TITLE = "Oracle Sequencer" + WIKI_DESCRIPTION = "Detection of oracle sequencer." + WIKI_RECOMMENDATION = "If you deploy contracts on the second layer as Arbitrum, you should perform an additional check if the sequencer is active. For more information visit https://docs.chain.link/data-feeds/l2-sequencer-feeds#available-networks" + + def _detect(self): + results = [] + output = [] + super()._detect() + if len(self.oracles) > 0: + for oracle in self.oracles: + results.append( + f"Oracle call to {oracle.contract}.{oracle.interface} ({oracle.node.source_mapping}) is used. Additional checks for sequencer lifeness should be implemented if deployed on the second layer.\n" + ) + res = self.generate_result(results) + output.append(res) + return output diff --git a/slither/detectors/oracles/supported_oracles/__init__.py b/slither/detectors/oracles/supported_oracles/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/slither/detectors/oracles/supported_oracles/chainlink_oracle.py b/slither/detectors/oracles/supported_oracles/chainlink_oracle.py new file mode 100644 index 000000000..277f32d8a --- /dev/null +++ b/slither/detectors/oracles/supported_oracles/chainlink_oracle.py @@ -0,0 +1,111 @@ +from enum import Enum +from slither.detectors.oracles.supported_oracles.oracle import Oracle, VarInCondition +from slither.slithir.operations import ( + Binary, + BinaryType, +) +from slither.slithir.variables.constant import Constant + + +CHAINLINK_ORACLE_CALLS = [ + "latestRoundData", + "getRoundData", +] +INTERFACES = ["AggregatorV3Interface", "FeedRegistryInterface"] + + +class ChainlinkVars(Enum): + ROUNDID = 0 + ANSWER = 1 + STARTEDAT = 2 + UPDATEDAT = 3 + ANSWEREDINROUND = 4 + + +class ChainlinkOracle(Oracle): + def __init__(self): + super().__init__(CHAINLINK_ORACLE_CALLS, INTERFACES) + + @staticmethod + def generate_naive_order(): + vars_order = {} + vars_order[ChainlinkVars.ROUNDID.value] = None + vars_order[ChainlinkVars.ANSWER.value] = None + vars_order[ChainlinkVars.STARTEDAT.value] = None + vars_order[ChainlinkVars.UPDATEDAT.value] = None + vars_order[ChainlinkVars.ANSWEREDINROUND.value] = None + return vars_order + + def find_which_vars_are_used(self): + vars_order = self.generate_naive_order() + for i in range(len(self.oracle_vars)): # pylint: disable=consider-using-enumerate + vars_order[self.returned_vars_indexes[i]] = self.oracle_vars[i] + return vars_order + + def is_needed_to_check_conditions(self, var): + if isinstance(var, VarInCondition): + var = var.var + if var in self.vars_not_in_condition: + return False + return True + + @staticmethod + def price_check_for_liveness(ir: Binary) -> bool: + if ir.type is (BinaryType.EQUAL): + if isinstance(ir.variable_right, Constant): + if ir.variable_right.value == 1: + return True + return False + + def is_sequencer_check(self, answer, startedAt): + if answer is None or startedAt is None: + return False + answer_checked = False + startedAt_checked = False + + if hasattr(answer, "nodes_with_var"): + for node in answer.nodes_with_var: + for ir in node.irs: + if isinstance(ir, Binary): + if self.price_check_for_liveness(ir): + answer_checked = True + startedAt_checked = self.check_staleness(startedAt) + + return answer_checked and startedAt_checked + + def naive_data_validation(self): + vars_order = self.find_which_vars_are_used() + problems = [] + # Iterating through all oracle variables which were returned by the oracle call + for (index, var) in vars_order.items(): + if not self.is_needed_to_check_conditions(var): + continue + # Second variable is the price value + if index == ChainlinkVars.ANSWER.value: + if not self.check_price(var): + problems.append( + f"The price value is validated incorrectly. This value is returned by Chainlink oracle call {self.contract}.{self.interface}.{self.oracle_api} ({self.node.source_mapping}).\n" + ) + # Third variable is the updatedAt value, indicating when the price was updated + elif index == ChainlinkVars.UPDATEDAT.value: + if not self.check_staleness(var): + problems.append( + f"The price can be stale due to incorrect validation of updatedAt value. This value is returned by Chainlink oracle call {self.contract}.{self.interface}.{self.oracle_api} ({self.node.source_mapping}).\n" + ) + + # Fourth variable is the startedAt value, indicating when the round was started. + # Used in connection with sequencer + elif ( + index == ChainlinkVars.STARTEDAT.value + and vars_order[ChainlinkVars.STARTEDAT.value] is not None + ): + # If the startedAt is not None. We are checking if the oracle is a sequencer to ignore incorrect results. + if self.is_sequencer_check(vars_order[ChainlinkVars.ANSWER.value], var): + problems = [] + break + # Iterate through checks performed out of the function where the oracle call is performed + for tup in self.out_of_function_checks: + problems.append( + f"The variation of {tup[0]} is checked on the lines {[str(node.source_mapping) for node in tup[1][::5]]}. Not in the original function where the Oracle call is performed.\n" + ) + return problems diff --git a/slither/detectors/oracles/supported_oracles/help_functions.py b/slither/detectors/oracles/supported_oracles/help_functions.py new file mode 100644 index 000000000..a4c265b24 --- /dev/null +++ b/slither/detectors/oracles/supported_oracles/help_functions.py @@ -0,0 +1,43 @@ +from slither.core.cfg.node import Node, NodeType +from slither.slithir.operations.return_operation import Return +from slither.slithir.operations import InternalCall +from slither.slithir.operations.solidity_call import SolidityCall +from slither.slithir.variables.constant import Constant + +# Helpfull functions + +# Check if the node's sons contain a revert statement +def check_revert(node: Node) -> bool: + for n in node.sons: + if n.type == NodeType.EXPRESSION: + for ir in n.irs: + if isinstance(ir, SolidityCall): + if "revert" in ir.function.name: + return True + return False + + +def is_boolean(ir) -> bool: + for val in ir.values: + if isinstance(val, Constant): + if isinstance(val.value, bool): + return True + return False + + +# Check if the node's sons contain a return statement +def return_boolean(node: Node) -> bool: + for n in node.sons: + if n.type == NodeType.RETURN: + for ir in n.irs: + if isinstance(ir, Return): + return is_boolean(ir) + return False + + +# Check if the node is an internal call +def is_internal_call(node) -> bool: + for ir in node.irs: + if isinstance(ir, InternalCall): + return True + return False diff --git a/slither/detectors/oracles/supported_oracles/oracle.py b/slither/detectors/oracles/supported_oracles/oracle.py new file mode 100644 index 000000000..7d874d5da --- /dev/null +++ b/slither/detectors/oracles/supported_oracles/oracle.py @@ -0,0 +1,135 @@ +from slither.slithir.operations import HighLevelCall, Operation +from slither.core.declarations import Function +from slither.slithir.operations import ( + Binary, + BinaryType, +) +from slither.detectors.oracles.supported_oracles.help_functions import check_revert, return_boolean +from slither.slithir.variables.constant import Constant + +# This class was created to store variable and all conditional nodes where it is used +class VarInCondition: # pylint: disable=too-few-public-methods + def __init__(self, _var, _nodes): + self.var = _var + self.nodes_with_var = _nodes + + +class Oracle: # pylint: disable=too-few-public-methods, too-many-instance-attributes + def __init__(self, _calls, _interfaces): + self.calls = _calls + self.interfaces = _interfaces + self.contract = None + self.function = None + self.node = None + self.out_of_function_checks = [] + self.oracle_vars = [] + self.vars_not_in_condition = [] + self.returned_vars_indexes = None + self.interface = None + self.oracle_api = None + ## Works as protection against recursion loop + self.occured_in_functions = [] + + # This function adds function to the list of functions where the oracle data were used + # Used to prevent recursion of visiting same functions + def add_occurance_in_function(self, _function): + if _function in self.occured_in_functions: + return False + self.occured_in_functions.append(_function) + return True + + def remove_occurances_in_function(self): + self.occured_in_functions = [] + + def get_calls(self): + return self.calls + + def is_instance_of(self, ir: Operation) -> bool: + return isinstance(ir, HighLevelCall) and ( + isinstance(ir.function, Function) + and self.compare_call(ir.function.name) + and hasattr(ir.destination, "type") + and self.compare_interface(str(ir.destination.type)) + ) + + def set_node(self, _node): + self.node = _node + + def set_function(self, _function): + self.function = _function + + def compare_interface(self, interface) -> bool: + if interface in self.interfaces: + return True + return False + + def compare_call(self, function) -> bool: + for call in self.calls: + if call in str(function): + return True + return False + + def set_data(self, _contract, _function, _returned_vars_indexes, _interface, _oracle_api): + self.contract = _contract + self.function = _function + self.returned_vars_indexes = _returned_vars_indexes + self.interface = _interface + self.oracle_api = _oracle_api + + # Data validation functions + def naive_data_validation(self): + return self + + @staticmethod + def check_greater_zero(ir: Operation) -> bool: + if isinstance(ir.variable_right, Constant): + if ir.type is (BinaryType.GREATER) or ir.type is (BinaryType.NOT_EQUAL): + if ir.variable_right.value == 0: + return True + elif isinstance(ir.variable_left, Constant): + if ir.type is (BinaryType.LESS) or ir.type is (BinaryType.NOT_EQUAL): + if ir.variable_left.value == 0: + return True + return False + + @staticmethod + def timestamp_in_node(node) -> bool: + all_nodes = [node] + if node.fathers: + all_nodes.extend(node.fathers) + for var in all_nodes: + if "block.timestamp" in str(var): + return True + return False + + # This function checks if the timestamp value is validated. + def check_staleness(self, var: VarInCondition) -> bool: + if var is None: + return False + different_behavior = False + if hasattr(var, "nodes_with_var"): + for node in var.nodes_with_var: + # This check look for conditions where the timestamp is used + if self.timestamp_in_node(node): + return True + if not different_behavior: + different_behavior = check_revert(node) or return_boolean(node) + + return different_behavior + + # This functions validates checks of price value + def check_price(self, var: VarInCondition) -> bool: + if var is None: + return False + different_behavior = False + if hasattr(var, "nodes_with_var"): + for node in var.nodes_with_var: + for ir in node.irs: + if isinstance(ir, Binary): + if self.check_greater_zero(ir): + return True + # If the conditions does not match we are looking for revert or return node + if not different_behavior: + different_behavior = check_revert(node) or return_boolean(node) + + return different_behavior diff --git a/slither/detectors/oracles/supported_oracles/tellor_oracle.py b/slither/detectors/oracles/supported_oracles/tellor_oracle.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/e2e/detectors/snapshots/detectors__detector_DeprecatedChainlinkCall_0_8_20_oracle_deprecated_call_sol__0.txt b/tests/e2e/detectors/snapshots/detectors__detector_DeprecatedChainlinkCall_0_8_20_oracle_deprecated_call_sol__0.txt new file mode 100644 index 000000000..1bf73b4ac --- /dev/null +++ b/tests/e2e/detectors/snapshots/detectors__detector_DeprecatedChainlinkCall_0_8_20_oracle_deprecated_call_sol__0.txt @@ -0,0 +1,2 @@ +Deprecated Chainlink call aggregator.latestAnswer used (tests/e2e/detectors/test_data/deprecated-chainlink-call/0.8.20/oracle_deprecated_call.sol#40). + diff --git a/tests/e2e/detectors/snapshots/detectors__detector_OracleDataCheck_0_8_20_oracle_check_out_of_function_sol__0.txt b/tests/e2e/detectors/snapshots/detectors__detector_OracleDataCheck_0_8_20_oracle_check_out_of_function_sol__0.txt new file mode 100644 index 000000000..fe94decd8 --- /dev/null +++ b/tests/e2e/detectors/snapshots/detectors__detector_OracleDataCheck_0_8_20_oracle_check_out_of_function_sol__0.txt @@ -0,0 +1,3 @@ +The price can be stale due to incorrect validation of updatedAt value. This value is returned by Chainlink oracle call ChainlinkETHUSDPriceConsumer.priceFeed.latestRoundData (tests/e2e/detectors/test_data/oracle-data-validation/0.8.20/oracle_check_out_of_function.sol#58). +The variation of price is checked on the lines ['tests/e2e/detectors/test_data/oracle-data-validation/0.8.20/oracle_check_out_of_function.sol#50']. Not in the original function where the Oracle call is performed. + diff --git a/tests/e2e/detectors/snapshots/detectors__detector_OracleDataCheck_0_8_20_oracle_data_check1_sol__0.txt b/tests/e2e/detectors/snapshots/detectors__detector_OracleDataCheck_0_8_20_oracle_data_check1_sol__0.txt new file mode 100644 index 000000000..f95daeacf --- /dev/null +++ b/tests/e2e/detectors/snapshots/detectors__detector_OracleDataCheck_0_8_20_oracle_data_check1_sol__0.txt @@ -0,0 +1,4 @@ +The price can be stale due to incorrect validation of updatedAt value. This value is returned by Chainlink oracle call OracleWithoutChecks.priceFeed.latestRoundData (test_data/oracle/0.8.20/oracle_data_check1.sol#46). + +The oracle OracleWithoutChecks.priceFeed (test_data/oracle/0.8.20/oracle_data_check1.sol#46) returns the variables ['price'] which are not validated. It can potentially lead to unexpected behaviour. + diff --git a/tests/e2e/detectors/snapshots/detectors__detector_OracleDataCheck_0_8_20_oracle_data_check2_sol__0.txt b/tests/e2e/detectors/snapshots/detectors__detector_OracleDataCheck_0_8_20_oracle_data_check2_sol__0.txt new file mode 100644 index 000000000..db5c3b493 --- /dev/null +++ b/tests/e2e/detectors/snapshots/detectors__detector_OracleDataCheck_0_8_20_oracle_data_check2_sol__0.txt @@ -0,0 +1,2 @@ +The price value is validated incorrectly. This value is returned by Chainlink oracle call StableOracleDAI.priceFeedDAIETH.latestRoundData (test_data/oracle/0.8.20/oracle_data_check2.sol#55-61). + diff --git a/tests/e2e/detectors/snapshots/detectors__detector_OracleDataCheck_0_8_20_oracle_data_check_price_in_double_internal_fc_sol__0.txt b/tests/e2e/detectors/snapshots/detectors__detector_OracleDataCheck_0_8_20_oracle_data_check_price_in_double_internal_fc_sol__0.txt new file mode 100644 index 000000000..e69de29bb diff --git a/tests/e2e/detectors/snapshots/detectors__detector_OracleDataCheck_0_8_20_oracle_data_check_price_in_internal_fc_sol__0.txt b/tests/e2e/detectors/snapshots/detectors__detector_OracleDataCheck_0_8_20_oracle_data_check_price_in_internal_fc_sol__0.txt new file mode 100644 index 000000000..e69de29bb diff --git a/tests/e2e/detectors/snapshots/detectors__detector_OracleDataCheck_0_8_20_oracle_non_revert_sol__0.txt b/tests/e2e/detectors/snapshots/detectors__detector_OracleDataCheck_0_8_20_oracle_non_revert_sol__0.txt new file mode 100644 index 000000000..e69de29bb diff --git a/tests/e2e/detectors/snapshots/detectors__detector_OracleDataCheck_0_8_20_oracle_timestamp_in_var_sol__0.txt b/tests/e2e/detectors/snapshots/detectors__detector_OracleDataCheck_0_8_20_oracle_timestamp_in_var_sol__0.txt new file mode 100644 index 000000000..dd42ee085 --- /dev/null +++ b/tests/e2e/detectors/snapshots/detectors__detector_OracleDataCheck_0_8_20_oracle_timestamp_in_var_sol__0.txt @@ -0,0 +1,2 @@ +The oracle ChainlinkETHUSDPriceConsumer.priceFeed (tests/e2e/detectors/test_data/oracle-data-validation/0.8.20/oracle_timestamp_in_var.sol#51) returns the variables ['price'] which are not validated. It can potentially lead to unexpected behaviour. + diff --git a/tests/e2e/detectors/test_data/deprecated-chainlink-call/0.8.20/oracle_deprecated_call.sol b/tests/e2e/detectors/test_data/deprecated-chainlink-call/0.8.20/oracle_deprecated_call.sol new file mode 100644 index 000000000..a64f2f36d --- /dev/null +++ b/tests/e2e/detectors/test_data/deprecated-chainlink-call/0.8.20/oracle_deprecated_call.sol @@ -0,0 +1,46 @@ +pragma solidity 0.8.20; + +interface AggregatorV3Interface { + function decimals() external view returns (uint8); + + function description() external view returns (string memory); + + function version() external view returns (uint256); + + function getRoundData( + uint80 _roundId + ) + external + view + returns ( + uint80 roundId, + int256 answer, + uint256 startedAt, + uint256 updatedAt, + uint80 answeredInRound + ); + + function latestRoundData() + external + view + returns ( + uint80 roundId, + int256 answer, + uint256 startedAt, + uint256 updatedAt, + uint80 answeredInRound + ); + + function latestAnswer() external view returns (int256); +} + +contract Oracle { + function getCurrentPrice(address _asset) external view returns (uint256) { + AggregatorV3Interface aggregator = AggregatorV3Interface(_asset); + int256 answer = aggregator.latestAnswer(); + require( + answer > 0, + "ChainlinkOracleManager: No pricing data available" + ); + } +} diff --git a/tests/e2e/detectors/test_data/deprecated-chainlink-call/0.8.20/oracle_deprecated_call.sol-0.8.20.zip b/tests/e2e/detectors/test_data/deprecated-chainlink-call/0.8.20/oracle_deprecated_call.sol-0.8.20.zip new file mode 100644 index 000000000..ddf687a7e Binary files /dev/null and b/tests/e2e/detectors/test_data/deprecated-chainlink-call/0.8.20/oracle_deprecated_call.sol-0.8.20.zip differ diff --git a/tests/e2e/detectors/test_data/oracle-data-validation/0.8.20/oracle_check_out_of_function.sol b/tests/e2e/detectors/test_data/oracle-data-validation/0.8.20/oracle_check_out_of_function.sol new file mode 100644 index 000000000..053806f3b --- /dev/null +++ b/tests/e2e/detectors/test_data/oracle-data-validation/0.8.20/oracle_check_out_of_function.sol @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.20; + +interface AggregatorV3Interface { + function decimals() external view returns (uint8); + + function description() external view returns (string memory); + + function version() external view returns (uint256); + + // getRoundData and latestRoundData should both raise "No data present" + // if they do not have data to report, instead of returning unset values + // which could be misinterpreted as actual reported values. + function getRoundData( + uint80 _roundId + ) + external + view + returns ( + uint80 roundId, + int256 answer, + uint256 startedAt, + uint256 updatedAt, + uint80 answeredInRound + ); + + function latestRoundData() + external + view + returns ( + uint80 roundId, + int256 answer, + uint256 startedAt, + uint256 updatedAt, + uint80 answeredInRound + ); +} + +contract ChainlinkETHUSDPriceConsumer { + AggregatorV3Interface internal priceFeed; + + constructor() public { + priceFeed = AggregatorV3Interface( + 0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419 + ); + } + + function obtainValidatedPrice() public view returns (int) { + int price = getLatestPrice(); + require(price > 0, "Price is not valid"); + return price; + } + + /** + * Returns the latest price + */ + function getLatestPrice() public view returns (int) { + (, int price, , , ) = priceFeed.latestRoundData(); + return price; + } + + function getDecimals() public view returns (uint8) { + return priceFeed.decimals(); + } +} diff --git a/tests/e2e/detectors/test_data/oracle-data-validation/0.8.20/oracle_check_out_of_function.sol-0.8.20.zip b/tests/e2e/detectors/test_data/oracle-data-validation/0.8.20/oracle_check_out_of_function.sol-0.8.20.zip new file mode 100644 index 000000000..d89ebf2e9 Binary files /dev/null and b/tests/e2e/detectors/test_data/oracle-data-validation/0.8.20/oracle_check_out_of_function.sol-0.8.20.zip differ diff --git a/tests/e2e/detectors/test_data/oracle-data-validation/0.8.20/oracle_data_check1.sol b/tests/e2e/detectors/test_data/oracle-data-validation/0.8.20/oracle_data_check1.sol new file mode 100644 index 000000000..7c9b99b6e --- /dev/null +++ b/tests/e2e/detectors/test_data/oracle-data-validation/0.8.20/oracle_data_check1.sol @@ -0,0 +1,50 @@ +pragma solidity 0.8.20; + +interface AggregatorV3Interface { + function decimals() external view returns (uint8); + + function description() external view returns (string memory); + + function version() external view returns (uint256); + + function getRoundData( + uint80 _roundId + ) + external + view + returns ( + uint80 roundId, + int256 answer, + uint256 startedAt, + uint256 updatedAt, + uint80 answeredInRound + ); + + function latestRoundData() + external + view + returns ( + uint80 roundId, + int256 answer, + uint256 startedAt, + uint256 updatedAt, + uint80 answeredInRound + ); +} + +contract OracleWithoutChecks { + AggregatorV3Interface priceFeed; + + constructor() { + priceFeed = AggregatorV3Interface( + 0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419 + ); + } + + function getPriceUSD() external view returns (uint256) { + //(uint80 roundID, int256 price, uint256 startedAt, uint256 timeStamp, uint80 answeredInRound) = priceFeed.latestRoundData(); + (, int256 price, , , ) = priceFeed.latestRoundData(); + // chainlink price data is 8 decimals for WETH/USD + return uint256(price) * 1e10; + } +} diff --git a/tests/e2e/detectors/test_data/oracle-data-validation/0.8.20/oracle_data_check1.sol-0.8.20.zip b/tests/e2e/detectors/test_data/oracle-data-validation/0.8.20/oracle_data_check1.sol-0.8.20.zip new file mode 100644 index 000000000..35d6cd640 Binary files /dev/null and b/tests/e2e/detectors/test_data/oracle-data-validation/0.8.20/oracle_data_check1.sol-0.8.20.zip differ diff --git a/tests/e2e/detectors/test_data/oracle-data-validation/0.8.20/oracle_data_check2.sol b/tests/e2e/detectors/test_data/oracle-data-validation/0.8.20/oracle_data_check2.sol new file mode 100644 index 000000000..da3a6e481 --- /dev/null +++ b/tests/e2e/detectors/test_data/oracle-data-validation/0.8.20/oracle_data_check2.sol @@ -0,0 +1,71 @@ +interface AggregatorV3Interface { + function decimals() external view returns (uint8); + + function description() external view returns (string memory); + + function version() external view returns (uint256); + + function getRoundData( + uint80 _roundId + ) + external + view + returns ( + uint80 roundId, + int256 answer, + uint256 startedAt, + uint256 updatedAt, + uint80 answeredInRound + ); + + function latestRoundData() + external + view + returns ( + uint80 roundId, + int256 answer, + uint256 startedAt, + uint256 updatedAt, + uint80 answeredInRound + ); +} + +contract StableOracleDAI { + AggregatorV3Interface priceFeedDAIETH; + + constructor() { + priceFeedDAIETH = AggregatorV3Interface( + 0x773616E4d11A78F511299002da57A0a94577F1f4 + ); + } + + function price_check(int price) internal view returns (bool) { + if (price > 0) { + return true; + } + return false; + } + + function getPriceUSD() external view returns (uint256) { + uint256 wethPriceUSD = 1; + uint256 DAIWethPrice = 1; + + // chainlink price data is 8 decimals for WETH/USD, so multiply by 10 decimals to get 18 decimal fractional + //(uint80 roundID, int256 price, uint256 startedAt, uint256 timeStamp, uint80 answeredInRound) = priceFeedDAIETH.latestRoundData(); + ( + uint80 roundID, + int256 price, + , + uint256 updatedAt, + uint80 answeredInRound + ) = priceFeedDAIETH.latestRoundData(); + // bool val = price_check(price); + require(price == 0); + require(updatedAt - block.timestamp < 500); + require(answeredInRound > roundID); + + return + (wethPriceUSD * 1e18) / + ((DAIWethPrice + uint256(price) * 1e10) / 2); + } +} diff --git a/tests/e2e/detectors/test_data/oracle-data-validation/0.8.20/oracle_data_check2.sol-0.8.20.zip b/tests/e2e/detectors/test_data/oracle-data-validation/0.8.20/oracle_data_check2.sol-0.8.20.zip new file mode 100644 index 000000000..2e62ecbc2 Binary files /dev/null and b/tests/e2e/detectors/test_data/oracle-data-validation/0.8.20/oracle_data_check2.sol-0.8.20.zip differ diff --git a/tests/e2e/detectors/test_data/oracle-data-validation/0.8.20/oracle_data_check_price_in_double_internal_fc.sol b/tests/e2e/detectors/test_data/oracle-data-validation/0.8.20/oracle_data_check_price_in_double_internal_fc.sol new file mode 100644 index 000000000..37f9a8bff --- /dev/null +++ b/tests/e2e/detectors/test_data/oracle-data-validation/0.8.20/oracle_data_check_price_in_double_internal_fc.sol @@ -0,0 +1,79 @@ +interface AggregatorV3Interface { + function decimals() external view returns (uint8); + + function description() external view returns (string memory); + + function version() external view returns (uint256); + + function getRoundData( + uint80 _roundId + ) + external + view + returns ( + uint80 roundId, + int256 answer, + uint256 startedAt, + uint256 updatedAt, + uint80 answeredInRound + ); + + function latestRoundData() + external + view + returns ( + uint80 roundId, + int256 answer, + uint256 startedAt, + uint256 updatedAt, + uint80 answeredInRound + ); +} + +contract StableOracleDAI { + AggregatorV3Interface priceFeedDAIETH; + + constructor() { + priceFeedDAIETH = AggregatorV3Interface( + 0x773616E4d11A78F511299002da57A0a94577F1f4 + ); + } + + function price_check(int price1) internal pure returns (bool) { + if (price_check2(price1)) { + return true; + } + return false; + } + + function price_check2(int price2) internal pure returns (bool) { + if (price2 > 0) { + return true; + } + return false; + } + + // Returns the latest price after validating it to be greater than zero and checking for data staleness and round completion. + function getPriceUSD() external view returns (uint256) { + uint256 wethPriceUSD = 1; + uint256 DAIWethPrice = 1; + + // chainlink price data is 8 decimals for WETH/USD, so multiply by 10 decimals to get 18 decimal fractional + //(uint80 roundID, int256 price, uint256 startedAt, uint256 timeStamp, uint80 answeredInRound) = priceFeedDAIETH.latestRoundData(); + ( + uint80 roundID, + int256 price, + , + uint256 updatedAt, + uint80 answeredInRound + ) = priceFeedDAIETH.latestRoundData(); + // bool val = price_check(price); + require(price_check(price)); + require(updatedAt - block.timestamp < 500); + require(answeredInRound > roundID); + + return + (wethPriceUSD * 1e18) / + ((DAIWethPrice + uint256(price) * 1e10) / 2); + } +} diff --git a/tests/e2e/detectors/test_data/oracle-data-validation/0.8.20/oracle_data_check_price_in_double_internal_fc.sol-0.8.20.zip b/tests/e2e/detectors/test_data/oracle-data-validation/0.8.20/oracle_data_check_price_in_double_internal_fc.sol-0.8.20.zip new file mode 100644 index 000000000..27314b899 Binary files /dev/null and b/tests/e2e/detectors/test_data/oracle-data-validation/0.8.20/oracle_data_check_price_in_double_internal_fc.sol-0.8.20.zip differ diff --git a/tests/e2e/detectors/test_data/oracle-data-validation/0.8.20/oracle_data_check_price_in_internal_fc.sol b/tests/e2e/detectors/test_data/oracle-data-validation/0.8.20/oracle_data_check_price_in_internal_fc.sol new file mode 100644 index 000000000..f4697213d --- /dev/null +++ b/tests/e2e/detectors/test_data/oracle-data-validation/0.8.20/oracle_data_check_price_in_internal_fc.sol @@ -0,0 +1,68 @@ +interface AggregatorV3Interface { + function decimals() external view returns (uint8); + + function description() external view returns (string memory); + + function version() external view returns (uint256); + + function getRoundData( + uint80 _roundId + ) + external + view + returns ( + uint80 roundId, + int256 answer, + uint256 startedAt, + uint256 updatedAt, + uint80 answeredInRound + ); + + function latestRoundData() + external + view + returns ( + uint80 roundId, + int256 answer, + uint256 startedAt, + uint256 updatedAt, + uint80 answeredInRound + ); +} + +contract StableOracleDAI { + AggregatorV3Interface priceFeedDAIETH; + + constructor() { + priceFeedDAIETH = AggregatorV3Interface( + 0x773616E4d11A78F511299002da57A0a94577F1f4 + ); + } + + function price_check(int price1) internal pure returns (bool) { + if (price1 > 0) { + return true; + } + return false; + } + + function getPriceUSD() external view returns (uint256) { + uint256 wethPriceUSD = 1; + uint256 DAIWethPrice = 1; + ( + uint80 roundID, + int256 price, + , + uint256 updatedAt, + uint80 answeredInRound + ) = priceFeedDAIETH.latestRoundData(); + // bool val = price_check(price); + require(price_check(price)); + require(updatedAt - block.timestamp < 500); + require(answeredInRound > roundID); + + return + (wethPriceUSD * 1e18) / + ((DAIWethPrice + uint256(price) * 1e10) / 2); + } +} diff --git a/tests/e2e/detectors/test_data/oracle-data-validation/0.8.20/oracle_data_check_price_in_internal_fc.sol-0.8.20.zip b/tests/e2e/detectors/test_data/oracle-data-validation/0.8.20/oracle_data_check_price_in_internal_fc.sol-0.8.20.zip new file mode 100644 index 000000000..91cd68028 Binary files /dev/null and b/tests/e2e/detectors/test_data/oracle-data-validation/0.8.20/oracle_data_check_price_in_internal_fc.sol-0.8.20.zip differ diff --git a/tests/e2e/detectors/test_data/oracle-data-validation/0.8.20/oracle_non_revert.sol b/tests/e2e/detectors/test_data/oracle-data-validation/0.8.20/oracle_non_revert.sol new file mode 100644 index 000000000..b441e71e1 --- /dev/null +++ b/tests/e2e/detectors/test_data/oracle-data-validation/0.8.20/oracle_non_revert.sol @@ -0,0 +1,95 @@ +pragma solidity 0.8.20; + +// SPDX-License-Identifier: MIT + +interface AggregatorV3Interface { + function decimals() external view returns (uint8); + + function description() external view returns (string memory); + + function version() external view returns (uint256); + + function getRoundData( + uint80 _roundId + ) + external + view + returns ( + uint80 roundId, + int256 answer, + uint256 startedAt, + uint256 updatedAt, + uint80 answeredInRound + ); + + function latestRoundData() + external + view + returns ( + uint80 roundId, + int256 answer, + uint256 startedAt, + uint256 updatedAt, + uint80 answeredInRound + ); +} + +contract StableOracleDAI { + AggregatorV3Interface priceFeedDAIETH; + + constructor() { + priceFeedDAIETH = AggregatorV3Interface( + 0x773616E4d11A78F511299002da57A0a94577F1f4 + ); + } + + function price_check(int price) internal pure returns (bool) { + if (price > 0) { + return true; + } + return false; + } + + function check_timestamp(uint256 updatedAt) internal view returns (bool) { + if (updatedAt - block.timestamp < 500) { + return true; + } + return false; + } + + function check_roundID( + uint80 roundID, + uint80 answeredInRound + ) internal pure returns (bool) { + if (answeredInRound > roundID) { + return true; + } + return false; + } + + function oracle_call() internal view returns (bool, uint256) { + ( + uint80 roundID, + int256 price, + , + uint256 updatedAt, + uint80 answeredInRound + ) = priceFeedDAIETH.latestRoundData(); + bool errorPrice = price_check(price); + if ( + errorPrice == false || + check_timestamp(updatedAt) == false || + check_roundID(roundID, answeredInRound) == false + ) { + return (false, 0); + } + + return (true, uint256(price)); + } + + function getPriceUSD() external view returns (uint256) { + (bool problem, uint price) = oracle_call(); + require(problem); + return price; + } +} diff --git a/tests/e2e/detectors/test_data/oracle-data-validation/0.8.20/oracle_non_revert.sol-0.8.20.zip b/tests/e2e/detectors/test_data/oracle-data-validation/0.8.20/oracle_non_revert.sol-0.8.20.zip new file mode 100644 index 000000000..debab63c1 Binary files /dev/null and b/tests/e2e/detectors/test_data/oracle-data-validation/0.8.20/oracle_non_revert.sol-0.8.20.zip differ diff --git a/tests/e2e/detectors/test_data/oracle-data-validation/0.8.20/oracle_timestamp_in_var.sol b/tests/e2e/detectors/test_data/oracle-data-validation/0.8.20/oracle_timestamp_in_var.sol new file mode 100644 index 000000000..38b86827a --- /dev/null +++ b/tests/e2e/detectors/test_data/oracle-data-validation/0.8.20/oracle_timestamp_in_var.sol @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.20; + +interface AggregatorV3Interface { + function decimals() external view returns (uint8); + + function description() external view returns (string memory); + + function version() external view returns (uint256); + + // getRoundData and latestRoundData should both raise "No data present" + // if they do not have data to report, instead of returning unset values + // which could be misinterpreted as actual reported values. + function getRoundData( + uint80 _roundId + ) + external + view + returns ( + uint80 roundId, + int256 answer, + uint256 startedAt, + uint256 updatedAt, + uint80 answeredInRound + ); + + function latestRoundData() + external + view + returns ( + uint80 roundId, + int256 answer, + uint256 startedAt, + uint256 updatedAt, + uint80 answeredInRound + ); +} + +contract ChainlinkETHUSDPriceConsumer { + AggregatorV3Interface internal priceFeed; + + constructor() public { + priceFeed = AggregatorV3Interface( + 0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419 + ); + } + /** + * Returns the latest price. Ensures the price is not outdated by checking the `updatedAt` timestamp. + */ + function getLatestPrice() public view returns (int) { + (, int price, , uint256 updateAt, ) = priceFeed.latestRoundData(); + uint current_timestamp = block.timestamp; + require(current_timestamp - updateAt < 1 minutes, "Price is outdated"); + return price; + } + + function getDecimals() public view returns (uint8) { + return priceFeed.decimals(); + } +} diff --git a/tests/e2e/detectors/test_data/oracle-data-validation/0.8.20/oracle_timestamp_in_var.sol-0.8.20.zip b/tests/e2e/detectors/test_data/oracle-data-validation/0.8.20/oracle_timestamp_in_var.sol-0.8.20.zip new file mode 100644 index 000000000..854f0e5d4 Binary files /dev/null and b/tests/e2e/detectors/test_data/oracle-data-validation/0.8.20/oracle_timestamp_in_var.sol-0.8.20.zip differ diff --git a/tests/e2e/detectors/test_detectors.py b/tests/e2e/detectors/test_detectors.py index 5604b57dd..0077efdab 100644 --- a/tests/e2e/detectors/test_detectors.py +++ b/tests/e2e/detectors/test_detectors.py @@ -1869,6 +1869,16 @@ def id_test(test_item: Test): "C.sol", "0.8.16", ), + Test(all_detectors.OracleDataCheck, "oracle_data_check1.sol", "0.8.20"), + Test(all_detectors.OracleDataCheck, "oracle_data_check2.sol", "0.8.20"), + Test( + all_detectors.OracleDataCheck, "oracle_data_check_price_in_double_internal_fc.sol", "0.8.20" + ), + Test(all_detectors.OracleDataCheck, "oracle_data_check_price_in_internal_fc.sol", "0.8.20"), + Test(all_detectors.OracleDataCheck, "oracle_non_revert.sol", "0.8.20"), + Test(all_detectors.OracleDataCheck, "oracle_check_out_of_function.sol", "0.8.20"), + Test(all_detectors.OracleDataCheck, "oracle_timestamp_in_var.sol", "0.8.20"), + Test(all_detectors.DeprecatedChainlinkCall, "oracle_deprecated_call.sol", "0.8.20"), ] GENERIC_PATH = "/GENERIC_PATH"