diff --git a/slither/analyses/data_flow/analyses/reentrancy/analysis/analysis.py b/slither/analyses/data_flow/analyses/reentrancy/analysis/analysis.py new file mode 100644 index 0000000000..4ab0f2f03f --- /dev/null +++ b/slither/analyses/data_flow/analyses/reentrancy/analysis/analysis.py @@ -0,0 +1,154 @@ +from typing import Optional, Set, Union + +from slither.analyses.data_flow.analyses.reentrancy.analysis.domain import ( + DomainVariant, + ReentrancyDomain, +) +from slither.analyses.data_flow.analyses.reentrancy.core.state import State +from slither.analyses.data_flow.engine.analysis import Analysis +from slither.analyses.data_flow.engine.direction import Direction, Forward +from slither.analyses.data_flow.engine.domain import Domain +from slither.core.cfg.node import Node +from slither.core.declarations.function import Function +from slither.core.variables.state_variable import StateVariable +from slither.slithir.operations.event_call import EventCall +from slither.slithir.operations.high_level_call import HighLevelCall +from slither.slithir.operations.internal_call import InternalCall +from slither.slithir.operations.low_level_call import LowLevelCall +from slither.slithir.operations.operation import Operation +from slither.slithir.operations.send import Send +from slither.slithir.operations.transfer import Transfer + + +class ReentrancyAnalysis(Analysis): + def __init__(self): + self._direction = Forward() + + def domain(self) -> Domain: + return ReentrancyDomain.bottom() + + def direction(self) -> Direction: + return self._direction + + def bottom_value(self) -> Domain: + return ReentrancyDomain.bottom() + + def transfer_function(self, node: Node, domain: ReentrancyDomain, operation: Operation): + self.transfer_function_helper(node, domain, operation, private_functions_seen=set()) + + def transfer_function_helper( + self, + node: Node, + domain: ReentrancyDomain, + operation: Operation, + private_functions_seen: Optional[Set[Function]] = None, + ): + if private_functions_seen is None: + private_functions_seen = set() + + if domain.variant == DomainVariant.BOTTOM: + domain.variant = DomainVariant.STATE + domain.state = State() + + self._analyze_operation_by_type(operation, domain, node, private_functions_seen) + + def _analyze_operation_by_type( + self, + operation: Operation, + domain: ReentrancyDomain, + node: Node, + private_functions_seen: Set[Function], + ): + if isinstance(operation, EventCall): + self._handle_event_call_operation(operation, domain) + elif isinstance(operation, InternalCall): + self._handle_internal_call_operation(operation, domain, private_functions_seen) + elif isinstance(operation, (HighLevelCall, LowLevelCall, Transfer, Send)): + self._handle_abi_call_contract_operation(operation, domain, node) + + self._handle_storage(domain, node) + self._update_writes_after_calls(domain, node) + + def _handle_storage(self, domain: ReentrancyDomain, node: Node): + # Track state reads + for var in node.state_variables_read: + if isinstance(var, StateVariable) and var.is_stored: + domain.state.add_read(var, node) + # Track state writes + for var in node.state_variables_written: + if isinstance(var, StateVariable) and var.is_stored: + domain.state.add_written(var, node) + + def _update_writes_after_calls(self, domain: ReentrancyDomain, node: Node): + # Writes after any external call + if node in domain.state.calls: + for var_name, write_nodes in domain.state.written.items(): + for wn in write_nodes: + domain.state.add_write_after_call(var_name, wn) + # Writes after ETH-sending calls + if node in domain.state.send_eth: + for var_name, write_nodes in domain.state.written.items(): + for wn in write_nodes: + domain.state.add_write_after_call(var_name, wn) + + def _handle_internal_call_operation( + self, + operation: InternalCall, + domain: ReentrancyDomain, + private_functions_seen: Set[Function], + ): + function = operation.function + if not isinstance(function, Function) or function in private_functions_seen: + return + + private_functions_seen.add(function) + for node in function.nodes: + for internal_operation in node.irs: + if isinstance(internal_operation, (HighLevelCall, LowLevelCall, Transfer, Send)): + continue + self.transfer_function_helper( + node, + domain, + internal_operation, + private_functions_seen, + ) + # Mark cross-function reentrancy for written variables + for var_name in domain.state.written.keys(): + domain.state.add_cross_function(var_name, function) + + def _handle_abi_call_contract_operation( + self, + operation: Union[LowLevelCall, HighLevelCall, Send, Transfer], + domain: ReentrancyDomain, + node: Node, + ): + # Track all external calls - avoid duplicates + if operation.node not in domain.state.calls.get(node, set()): + domain.state.add_call(node, operation.node) + + # Track variables read prior to this call + for var_name in domain.state.reads.keys(): + domain.state.add_reads_prior_calls(node, var_name) + + # Track external calls that send ETH - avoid duplicates + if operation.can_send_eth: + if operation.node not in domain.state.send_eth.get(node, set()): + domain.state.add_send_eth(node, operation.node) + + def _handle_event_call_operation(self, operation: EventCall, domain: ReentrancyDomain): + # Track events and propagate previous external calls + # Only propagate calls that haven't already been propagated to this event node + existing_calls = domain.state.calls.get(operation.node, set()) + + # Collect all calls to add before modifying the dictionary + calls_to_add = [] + for calls_set in domain.state.calls.values(): + for call_node in calls_set: + if call_node not in existing_calls: + calls_to_add.append(call_node) + + # Add all collected calls + for call_node in calls_to_add: + domain.state.add_call(operation.node, call_node) + + domain.state.add_event(operation, operation.node) diff --git a/slither/analyses/data_flow/analyses/reentrancy/analysis/domain.py b/slither/analyses/data_flow/analyses/reentrancy/analysis/domain.py new file mode 100644 index 0000000000..0b095b3d00 --- /dev/null +++ b/slither/analyses/data_flow/analyses/reentrancy/analysis/domain.py @@ -0,0 +1,60 @@ +from enum import Enum, auto +from typing import Optional + +from slither.analyses.data_flow.analyses.reentrancy.core.state import State +from slither.analyses.data_flow.engine.domain import Domain + + +class DomainVariant(Enum): + BOTTOM = auto() + TOP = auto() + STATE = auto() + + +class ReentrancyDomain(Domain): + def __init__(self, variant: DomainVariant, state: Optional[State] = None): + self.variant = variant + self.state = state or State() + + @classmethod + def bottom(cls) -> "ReentrancyDomain": + return cls(DomainVariant.BOTTOM) + + @classmethod + def top(cls) -> "ReentrancyDomain": + return cls(DomainVariant.TOP) + + @classmethod + def with_state(cls, info: State) -> "ReentrancyDomain": + return cls(DomainVariant.STATE, info) + + def join(self, other: "ReentrancyDomain") -> bool: + if self.variant == DomainVariant.TOP or other.variant == DomainVariant.BOTTOM: + return False + + if self.variant == DomainVariant.BOTTOM and other.variant == DomainVariant.STATE: + self.variant = DomainVariant.STATE + self.state = other.state.deep_copy() + self.state.written.clear() + self.state.events.clear() + self.state.writes_after_calls.clear() + self.state.cross_function.clear() + return True + + if self.variant == DomainVariant.STATE and other.variant == DomainVariant.STATE: + if self.state == other.state: + return False + + self.state.send_eth.update(other.state.send_eth) + self.state.calls.update(other.state.calls) + self.state.reads.update(other.state.reads) + self.state.reads_prior_calls.update(other.state.reads_prior_calls) + self.state.safe_send_eth.update(other.state.safe_send_eth) + self.state.writes_after_calls.update(other.state.writes_after_calls) + self.state.cross_function.update(other.state.cross_function) + return True + + else: + self.variant = DomainVariant.TOP + + return True diff --git a/slither/analyses/data_flow/analyses/reentrancy/core/state.py b/slither/analyses/data_flow/analyses/reentrancy/core/state.py new file mode 100644 index 0000000000..87e3c18540 --- /dev/null +++ b/slither/analyses/data_flow/analyses/reentrancy/core/state.py @@ -0,0 +1,162 @@ +import copy +from collections import defaultdict +from typing import Dict, Set + +from slither.core.cfg.node import Node +from slither.core.declarations.function import Function +from slither.core.variables.state_variable import StateVariable +from slither.slithir.operations.event_call import EventCall + + +class State: + def __init__(self): + self._send_eth: Dict[Node, Set[Node]] = defaultdict(set) + self._safe_send_eth: Dict[Node, Set[Node]] = defaultdict(set) + self._calls: Dict[Node, Set[Node]] = defaultdict(set) + self._reads: Dict[str, Set[Node]] = defaultdict(set) + self._reads_prior_calls: Dict[Node, Set[str]] = defaultdict(set) + self._events: Dict[EventCall, Set[Node]] = defaultdict(set) + self._written: Dict[str, Set[Node]] = defaultdict(set) + self.writes_after_calls: Dict[str, Set[Node]] = defaultdict(set) + self.cross_function: Dict[StateVariable, Set[Function]] = defaultdict(set) + + # -------------------- Add methods -------------------- + def add_call(self, node: Node, call_node: Node): + self._calls[node].add(call_node) + + def add_send_eth(self, node: Node, call_node: Node): + self._send_eth[node].add(call_node) + + def add_safe_send_eth(self, node: Node, call_node: Node): + self._safe_send_eth[node].add(call_node) + + def add_written(self, var: StateVariable, node: Node): + # Ensure the canonical name exists and is not None + if var.canonical_name is not None: + # Ensure the key exists in the defaultdict + if var.canonical_name not in self._written: + self._written[var.canonical_name] = set() + self._written[var.canonical_name].add(node) + + def add_read(self, var: StateVariable, node: Node): + # Ensure the canonical name exists and is not None + if var.canonical_name is not None: + # Ensure the key exists in the defaultdict + if var.canonical_name not in self._reads: + self._reads[var.canonical_name] = set() + self._reads[var.canonical_name].add(node) + + def add_reads_prior_calls(self, node: Node, var_name: str): + self._reads_prior_calls[node].add(var_name) + + def add_write_after_call(self, var_name: str, node: Node): + self.writes_after_calls[var_name].add(node) + + def add_cross_function(self, var: StateVariable, function: Function): + self.cross_function[var].add(function) + + def add_event(self, event: EventCall, node: Node): + self._events[event].add(node) + + # -------------------- Properties -------------------- + @property + def send_eth(self) -> Dict[Node, Set[Node]]: + return self._send_eth + + @property + def safe_send_eth(self) -> Dict[Node, Set[Node]]: + return self._safe_send_eth + + @property + def all_eth_calls(self) -> Dict[Node, Set[Node]]: + result = defaultdict(set) + for node, calls in self._send_eth.items(): + result[node].update(calls) + for node, calls in self._safe_send_eth.items(): + result[node].update(calls) + return result + + @property + def calls(self) -> Dict[Node, Set[Node]]: + return self._calls + + @property + def reads(self) -> Dict[str, Set[Node]]: + return self._reads + + @property + def written(self) -> Dict[str, Set[Node]]: + return self._written + + @property + def reads_prior_calls(self) -> Dict[Node, Set[str]]: + return self._reads_prior_calls + + @property + def events(self) -> Dict[EventCall, Set[Node]]: + return self._events + + # -------------------- Utilities -------------------- + def __eq__(self, other): + if not isinstance(other, State): + return False + return ( + self._send_eth == other._send_eth + and self._safe_send_eth == other._safe_send_eth + and self._calls == other._calls + and self._reads == other._reads + and self._reads_prior_calls == other._reads_prior_calls + and self._events == other._events + and self._written == other._written + and self.writes_after_calls == other.writes_after_calls + and self.cross_function == other.cross_function + ) + + def __hash__(self): + return hash( + ( + frozenset(self._send_eth.items()), + frozenset(self._safe_send_eth.items()), + frozenset(self._calls.items()), + frozenset(self._reads.items()), + frozenset(self._reads_prior_calls.items()), + frozenset(self._events.items()), + frozenset(self._written.items()), + frozenset((k, frozenset(v)) for k, v in self.writes_after_calls.items()), + frozenset((k, frozenset(v)) for k, v in self.cross_function.items()), + ) + ) + + def __str__(self): + return ( + f"State(\n" + f" send_eth: {len(self._send_eth)} items,\n" + f" safe_send_eth: {len(self._safe_send_eth)} items,\n" + f" calls: {len(self._calls)} items,\n" + f" reads: {len(self._reads)} items,\n" + f" reads_prior_calls: {len(self._reads_prior_calls)} items,\n" + f" events: {len(self._events)} items,\n" + f" written: {len(self._written)} items,\n" + f" writes_after_calls: {len(self.writes_after_calls)} items,\n" + f" cross_function: {len(self.cross_function)} items,\n" + f")" + ) + + def deep_copy(self) -> "State": + new_state = State() + # Use shallow copy for Node objects to avoid circular reference issues + + new_state._send_eth.update({k: v.copy() for k, v in self._send_eth.items()}) + new_state._safe_send_eth.update({k: v.copy() for k, v in self._safe_send_eth.items()}) + new_state._calls.update({k: v.copy() for k, v in self._calls.items()}) + new_state._reads.update({k: v.copy() for k, v in self._reads.items()}) + new_state._reads_prior_calls.update( + {k: v.copy() for k, v in self._reads_prior_calls.items()} + ) + new_state._events.update({k: v.copy() for k, v in self._events.items()}) + new_state._written.update({k: v.copy() for k, v in self._written.items()}) + new_state.writes_after_calls.update( + {k: v.copy() for k, v in self.writes_after_calls.items()} + ) + new_state.cross_function.update({k: v.copy() for k, v in self.cross_function.items()}) + return new_state diff --git a/slither/analyses/data_flow/engine/analysis.py b/slither/analyses/data_flow/engine/analysis.py new file mode 100644 index 0000000000..5793333fec --- /dev/null +++ b/slither/analyses/data_flow/engine/analysis.py @@ -0,0 +1,42 @@ +from abc import ABC, abstractmethod +from typing import Generic, TypeVar + +from slither.analyses.data_flow.engine.direction import Direction +from slither.analyses.data_flow.engine.domain import Domain +from slither.core.cfg.node import Node +from slither.slithir.operations.operation import Operation + + +class Analysis(ABC): + @abstractmethod + def domain(self) -> Domain: + pass + + @abstractmethod + def direction(self) -> Direction: + pass + + @abstractmethod + def transfer_function(self, node: Node, domain: Domain, operation: Operation): + pass + + @abstractmethod + def bottom_value(self) -> Domain: + pass + + def apply_condition(self, domain: Domain, condition: Operation, branch_taken: bool) -> Domain: + """Override this to handle branch filtering. Default: no-op""" + return domain # Analyses that don't implement this get no filtering + + def apply_widening(self, current_state: Domain, previous_state: Domain, set_b: set) -> Domain: + """Override this to implement widening operations. Default: no-op""" + return current_state # Analyses that don't implement this get no widening + + +A = TypeVar("A", bound=Analysis) + + +class AnalysisState(Generic[A]): + def __init__(self, pre: Domain, post: Domain) -> None: + self.pre: Domain = pre + self.post: Domain = post diff --git a/slither/analyses/data_flow/engine/direction.py b/slither/analyses/data_flow/engine/direction.py new file mode 100644 index 0000000000..6b63dbdeac --- /dev/null +++ b/slither/analyses/data_flow/engine/direction.py @@ -0,0 +1,72 @@ +from abc import ABC, abstractmethod +from typing import TYPE_CHECKING, Deque, Dict + +if TYPE_CHECKING: + from slither.analyses.data_flow.engine.analysis import A, Analysis, AnalysisState + +from slither.core.cfg.node import Node + + +class Direction(ABC): + @property + @abstractmethod + def IS_FORWARD(self) -> bool: + pass + + @abstractmethod + def apply_transfer_function( + self, + analysis: "Analysis", + current_state: "AnalysisState", + node: Node, + worklist: Deque[Node], + global_state: Dict[int, "AnalysisState[A]"], + ): + pass + + +class Forward(Direction): + def __init__(self): + pass + + @property + def IS_FORWARD(self) -> bool: + return True + + def apply_transfer_function( + self, + analysis: "Analysis", + current_state: "AnalysisState", + node: Node, + worklist: Deque[Node], + global_state: Dict[int, "AnalysisState[A]"], + ): + # Apply transfer function to current node + for operation in node.irs or [None]: + analysis.transfer_function(node=node, domain=current_state.pre, operation=operation) + + # Set post state + global_state[node.node_id].post = current_state.pre + + # Propagate to all successors + for successor in node.sons: + if successor and successor.node_id in global_state: + son_state = global_state[successor.node_id] + if son_state.pre.join(current_state.pre) and successor not in worklist: + worklist.append(successor) + + +class Backward(Direction): + @property + def IS_FORWARD(self) -> bool: + return False + + def apply_transfer_function( + self, + analysis: "Analysis", + current_state: "AnalysisState", + node: Node, + worklist: Deque[Node], + global_state: Dict[int, "AnalysisState[A]"], + ): + raise NotImplementedError("Backward transfer function hasn't been developed yet") diff --git a/slither/analyses/data_flow/engine/domain.py b/slither/analyses/data_flow/engine/domain.py new file mode 100644 index 0000000000..896d8b88c2 --- /dev/null +++ b/slither/analyses/data_flow/engine/domain.py @@ -0,0 +1,21 @@ +from abc import ABC, abstractmethod + +try: + from typing import Self +except ImportError: + from typing_extensions import Self + + +class Domain(ABC): + @abstractmethod + def top(cls) -> Self: + """The top element of the domain""" + + @abstractmethod + def bottom(cls) -> Self: + """The bottom element of the domain""" + + @abstractmethod + def join(self, other: Self) -> bool: + """Computes the least upper bound of two elements and store the result in self. + Return True if self changed.""" diff --git a/slither/analyses/data_flow/engine/engine.py b/slither/analyses/data_flow/engine/engine.py new file mode 100644 index 0000000000..5db09391a9 --- /dev/null +++ b/slither/analyses/data_flow/engine/engine.py @@ -0,0 +1,60 @@ +from collections import deque +from typing import Deque, Dict, Generic, List + +from slither.analyses.data_flow.engine.analysis import A, Analysis, AnalysisState +from slither.core.cfg.node import Node +from slither.core.declarations.function import Function + + +class Engine(Generic[A]): + def __init__(self): + self.state: Dict[int, AnalysisState[A]] = {} + self.nodes: List[Node] = [] + self.analysis: Analysis + self.function: Function # Single function being analyzed + + @classmethod + def new(cls, analysis: Analysis, function: Function): + engine = cls() + engine.analysis = analysis + engine.function = function # Store single function + + # Create state mapping for nodes in this single function only + # Data flow analysis operates on one function's CFG at a time + for node in function.nodes: + engine.nodes.append(node) + engine.state[node.node_id] = AnalysisState( + pre=analysis.bottom_value(), post=analysis.bottom_value() + ) + + return engine + + def run_analysis(self): + worklist: Deque[Node] = deque() + + if self.analysis.direction().IS_FORWARD: + if self.function.entry_point is not None: + worklist.append(self.function.entry_point) + else: + raise NotImplementedError("Backward analysis is not implemented") + + while worklist: + node = worklist.popleft() + + current_state = AnalysisState( + pre=self.state[node.node_id].pre, post=self.state[node.node_id].post + ) + + self.analysis.direction().apply_transfer_function( + analysis=self.analysis, + current_state=current_state, + node=node, + worklist=worklist, + global_state=self.state, + ) + + def result(self) -> Dict[Node, AnalysisState[A]]: + result = {} + for node in self.nodes: + result[node] = self.state[node.node_id] + return result diff --git a/slither/detectors/all_detectors.py b/slither/detectors/all_detectors.py index a30d2b3c00..9bc8944035 100644 --- a/slither/detectors/all_detectors.py +++ b/slither/detectors/all_detectors.py @@ -18,6 +18,8 @@ from .reentrancy.reentrancy_eth import ReentrancyEth from .reentrancy.reentrancy_no_gas import ReentrancyNoGas from .reentrancy.reentrancy_events import ReentrancyEvent +from .reentrancy_df.reentrancy_eth_df import ReentrancyEthDF + from .variables.unused_state_variables import UnusedStateVars from .variables.could_be_constant import CouldBeConstant from .variables.could_be_immutable import CouldBeImmutable diff --git a/slither/detectors/reentrancy/reentrancy.py b/slither/detectors/reentrancy/reentrancy.py index 8f39c519d2..d1afc13ed5 100644 --- a/slither/detectors/reentrancy/reentrancy.py +++ b/slither/detectors/reentrancy/reentrancy.py @@ -1,9 +1,10 @@ -"""" - Re-entrancy detection +""" " +Re-entrancy detection - Based on heuristics, it may lead to FP and FN - Iterate over all the nodes of the graph until reaching a fixpoint +Based on heuristics, it may lead to FP and FN +Iterate over all the nodes of the graph until reaching a fixpoint """ + from collections import defaultdict from typing import Set, Dict, List, Tuple, Optional diff --git a/slither/detectors/reentrancy_df/reentrancy_eth_df.py b/slither/detectors/reentrancy_df/reentrancy_eth_df.py new file mode 100644 index 0000000000..5aec2a4efc --- /dev/null +++ b/slither/detectors/reentrancy_df/reentrancy_eth_df.py @@ -0,0 +1,258 @@ +""" +Re-entrancy detection +""" + +from collections import defaultdict, namedtuple +from typing import Dict, List, Set +from loguru import logger + +from slither.analyses.data_flow.analyses.reentrancy.analysis.analysis import ReentrancyAnalysis +from slither.analyses.data_flow.analyses.reentrancy.analysis.domain import ( + DomainVariant, + ReentrancyDomain, +) +from slither.analyses.data_flow.engine.engine import Engine +from slither.core.variables.state_variable import StateVariable +from slither.detectors.abstract_detector import AbstractDetector, DetectorClassification +from slither.detectors.reentrancy.reentrancy import to_hashable +from slither.utils.output import Output + +FindingKey = namedtuple("FindingKey", ["function", "calls", "send_eth"]) +FindingValue = namedtuple("FindingValue", ["variable", "node", "nodes", "cross_functions"]) + + +class ReentrancyEthDF(AbstractDetector): + ARGUMENT = "reentrancy-eth-df" + HELP = "Reentrancy vulnerabilities (theft of ethers)" + IMPACT = DetectorClassification.HIGH + CONFIDENCE = DetectorClassification.MEDIUM + WIKI = ( + "https://github.com/crytic/slither/wiki/Detector-Documentation#reentrancy-vulnerabilities" + ) + WIKI_TITLE = "Reentrancy vulnerabilities" + + # region wiki_description + WIKI_DESCRIPTION = """ + Detection of the [reentrancy bug](https://github.com/trailofbits/not-so-smart-contracts/tree/master/reentrancy). + Do not report reentrancies that don't involve Ether (see reentrancy-no-eth) + """ + # endregion wiki_description + + # region wiki_exploit_scenario + WIKI_EXPLOIT_SCENARIO = """ + solidity + function withdrawBalance(){ + // send userBalance[msg.sender] Ether to msg.sender + // if msg.sender is a contract, it will call its fallback function + if( ! (msg.sender.call.value(userBalance[msg.sender])() ) ){ + throw; + } + userBalance[msg.sender] = 0; + } + Bob uses the re-entrancy bug to call withdrawBalance two times, and withdraw more than its initial deposit to the contract. + """ + # endregion wiki_exploit_scenario + + WIKI_RECOMMENDATION = "Apply the [check-effects-interactions pattern](http://solidity.readthedocs.io/en/v0.4.21/security-considerations.html#re-entrancy)." + STANDARD_JSON = False + + def find_reentrancies(self) -> Dict[FindingKey, Set[FindingValue]]: + """Detect per-function reentrancy vulnerabilities using data flow analysis.""" + result: Dict[FindingKey, Set[FindingValue]] = defaultdict(set) + + for contract in self.contracts: + variables_used_in_reentrancy = contract.state_variables_used_in_reentrant_targets + functions = [ + f + for f in contract.functions_and_modifiers_declared + if f.is_implemented and not f.is_constructor + ] + + for f in functions: + engine = Engine.new(analysis=ReentrancyAnalysis(), function=f) + engine.run_analysis() + engine_result = engine.result() + vulnerable_findings: Set[FindingValue] = set() + function_calls = {} + function_send_eth = {} + + for node in f.nodes: + if node not in engine_result: + continue + + analysis = engine_result[node] + if not hasattr(analysis, "post") or not isinstance( + analysis.post, ReentrancyDomain + ): + continue + if analysis.post.variant != DomainVariant.STATE: + continue + + state = analysis.post.state + + # Track calls and sends + for call_node, call_destinations in state.calls.items(): + function_calls.setdefault(call_node, set()).update(call_destinations) + for send_node, send_destinations in state.send_eth.items(): + function_send_eth.setdefault(send_node, set()).update(send_destinations) + + # Skip if not all reentrancy conditions are met: ETH calls, state writes, and reads before calls + if not ( + (state.send_eth or state.safe_send_eth) + and state.written + and state.reads_prior_calls + ): + continue + + # Iterate through all calls that have variables read before them + for call_node, vars_read_before_call in state.reads_prior_calls.items(): + # Check each variable that was read before a call + for var_canonical_name in vars_read_before_call: + # Find the actual StateVariable object by canonical name + var = next( + ( + sv + for sv in contract.state_variables + if sv.canonical_name == var_canonical_name + ), + None, + ) + # Skip if variable not found or not a StateVariable + if not var or not isinstance(var, StateVariable): + continue + + # Only proceed if this variable is written somewhere + if var_canonical_name not in state.written: + continue + + writing_nodes = state.written[var_canonical_name] + + # Filter out entry point nodes to avoid false positives + non_entry_writing_nodes = { + n for n in writing_nodes if n != f.entry_point + } + + # Skip if no non-entry writing nodes found + if not non_entry_writing_nodes: + continue + + # Only report if function is reentrant or variable used in cross-function reentrancy + if not (f.is_reentrant or var in variables_used_in_reentrancy): + continue + + cross_functions = variables_used_in_reentrancy.get(var, []) + + # Convert set to list if needed for consistent handling + if isinstance(cross_functions, set): + cross_functions = list(cross_functions) + + # Use the first writing node as the main node for reporting + main_node = min(non_entry_writing_nodes, key=lambda x: x.node_id) + + # Create finding value with all relevant information + finding_value = FindingValue( + var, + main_node, + tuple(sorted(non_entry_writing_nodes, key=lambda x: x.node_id)), + tuple(sorted(cross_functions, key=lambda x: str(x))), + ) + + # Add to vulnerable findings set + vulnerable_findings.add(finding_value) + + if vulnerable_findings: + finding_key = FindingKey( + function=f, + calls=to_hashable(function_calls), + send_eth=to_hashable(function_send_eth), + ) + result[finding_key] |= vulnerable_findings + + return result + + def _detect(self) -> List[Output]: + super()._detect() + reentrancies = self.find_reentrancies() + results = [] + + result_sorted = sorted(list(reentrancies.items()), key=lambda x: x[0].function.name) + + for (func, calls, send_eth), varsWrittenSet in result_sorted: + calls_dict = { + call_node.node_id: (call_node, call_list) for call_node, call_list in calls + } + calls_sorted = sorted(calls_dict.values(), key=lambda x: x[0].node_id) + send_eth_dict = { + send_node.node_id: (send_node, send_list) for send_node, send_list in send_eth + } + send_eth_sorted = sorted(send_eth_dict.values(), key=lambda x: x[0].node_id) + varsWritten = sorted(varsWrittenSet, key=lambda x: (x.variable.name, x.node.node_id)) + + info = ["Reentrancy in ", func, ":\n"] + info += ["\tExternal calls:\n"] + for call_node, calls_list in calls_sorted: + info += ["\t- ", call_node, "\n"] + for c in calls_list: + if c != call_node: + info += ["\t\t- ", c, "\n"] + + calls_node_ids = {call_node.node_id for call_node, _ in calls_sorted} + send_eth_node_ids = {send_node.node_id for send_node, _ in send_eth_sorted} + if calls_node_ids != send_eth_node_ids and send_eth_sorted: + info += ["\tExternal calls sending eth:\n"] + for send_node, send_list in send_eth_sorted: + info += ["\t- ", send_node, "\n"] + for s in send_list: + if s != send_node: + info += ["\t\t- ", s, "\n"] + + info += ["\tState variables written after the call(s):\n"] + for finding_value in varsWritten: + info += ["\t- ", finding_value.node, "\n"] + for other_node in finding_value.nodes: + if other_node != finding_value.node: + info += ["\t\t- ", other_node, "\n"] + if finding_value.cross_functions: + info += [ + "\t", + finding_value.variable, + " can be used in cross function reentrancies:\n", + ] + for cross in finding_value.cross_functions: + info += ["\t- ", cross, "\n"] + + res = self.generate_result(info) + res.add(func) + for call_node, calls_list in calls_sorted: + res.add(call_node, {"underlying_type": "external_calls"}) + for c in calls_list: + if c != call_node: + res.add(c, {"underlying_type": "external_calls_sending_eth"}) + + if calls_node_ids != send_eth_node_ids: + for send_node, send_list in send_eth_sorted: + res.add(send_node, {"underlying_type": "external_calls_sending_eth"}) + for s in send_list: + if s != send_node: + res.add(s, {"underlying_type": "external_calls_sending_eth"}) + + for finding_value in varsWritten: + res.add( + finding_value.node, + { + "underlying_type": "variables_written", + "variable_name": finding_value.variable.name, + }, + ) + for other_node in finding_value.nodes: + if other_node != finding_value.node: + res.add( + other_node, + { + "underlying_type": "variables_written", + "variable_name": finding_value.variable.name, + }, + ) + results.append(res) + + return results diff --git a/tests/e2e/detectors/snapshots/detectors__detector_ReentrancyEthDF_0_4_25_DAO_sol__0.txt b/tests/e2e/detectors/snapshots/detectors__detector_ReentrancyEthDF_0_4_25_DAO_sol__0.txt new file mode 100644 index 0000000000..2d6c2b8204 --- /dev/null +++ b/tests/e2e/detectors/snapshots/detectors__detector_ReentrancyEthDF_0_4_25_DAO_sol__0.txt @@ -0,0 +1,72 @@ +Reentrancy in TokenCreation.refund() (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/DAO.sol#318-332): + External calls: + - extraBalance.balance >= extraBalance.accumulatedInput() (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/DAO.sol#321) + - extraBalance.payOut(address(this),extraBalance.accumulatedInput()) (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/DAO.sol#322) + - msg.sender.call.value(weiGiven[msg.sender])() (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/DAO.sol#325) + External calls sending eth: + - msg.sender.call.value(weiGiven[msg.sender])() (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/DAO.sol#325) + State variables written after the call(s): + - weiGiven[msg.sender] = 0 (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/DAO.sol#329) + TokenCreationInterface.weiGiven (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/DAO.sol#251) can be used in cross function reentrancies: + - TokenCreation.createTokenProxy(address) (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/DAO.sol#299-316) + - TokenCreation.refund() (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/DAO.sol#318-332) + +Reentrancy in DAO.executeProposal(uint256,bytes) (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/DAO.sol#853-937): + External calls: + - ! isRecipientAllowed(p.recipient) (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/DAO.sol#881) + - allowedRecipients[_recipient] || (_recipient == address(extraBalance) && totalRewardToken > extraBalance.accumulatedInput()) (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/DAO.sol#1159-1163) + - ! p.recipient.call.value(p.amount)(_transactionData) (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/DAO.sol#915) + External calls sending eth: + - ! p.creator.send(p.proposalDeposit) (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/DAO.sol#904) + - ! p.recipient.call.value(p.amount)(_transactionData) (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/DAO.sol#915) + State variables written after the call(s): + - p.proposalPassed = true (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/DAO.sol#918) + DAOInterface.proposals (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/DAO.sol#394) can be used in cross function reentrancies: + - DAO.DAO(address,DAO_Creator,uint256,uint256,uint256,address) (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/DAO.sol#702-726) + - DAO.checkProposalCode(uint256,address,uint256,bytes) (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/DAO.sol#809-817) + - DAO.closeProposal(uint256) (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/DAO.sol#940-945) + - DAO.executeProposal(uint256,bytes) (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/DAO.sol#853-937) + - DAO.getNewDAOAddress(uint256) (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/DAO.sol#1204-1206) + - DAO.isBlocked(address) (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/DAO.sol#1208-1218) + - DAO.newProposal(address,uint256,string,bytes,uint256,bool) (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/DAO.sol#741-806) + - DAO.numberOfProposals() (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/DAO.sol#1199-1202) + - DAOInterface.proposals (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/DAO.sol#394) + - DAO.splitDAO(uint256,address) (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/DAO.sol#947-1020) + - DAO.vote(uint256,bool) (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/DAO.sol#820-850) + - closeProposal(_proposalID) (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/DAO.sol#933) + - p.open = false (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/DAO.sol#944) + DAOInterface.proposals (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/DAO.sol#394) can be used in cross function reentrancies: + - DAO.DAO(address,DAO_Creator,uint256,uint256,uint256,address) (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/DAO.sol#702-726) + - DAO.checkProposalCode(uint256,address,uint256,bytes) (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/DAO.sol#809-817) + - DAO.closeProposal(uint256) (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/DAO.sol#940-945) + - DAO.executeProposal(uint256,bytes) (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/DAO.sol#853-937) + - DAO.getNewDAOAddress(uint256) (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/DAO.sol#1204-1206) + - DAO.isBlocked(address) (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/DAO.sol#1208-1218) + - DAO.newProposal(address,uint256,string,bytes,uint256,bool) (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/DAO.sol#741-806) + - DAO.numberOfProposals() (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/DAO.sol#1199-1202) + - DAOInterface.proposals (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/DAO.sol#394) + - DAO.splitDAO(uint256,address) (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/DAO.sol#947-1020) + - DAO.vote(uint256,bool) (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/DAO.sol#820-850) + - rewardToken[address(this)] += p.amount (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/DAO.sol#928) + DAOInterface.rewardToken (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/DAO.sol#410) can be used in cross function reentrancies: + - DAO.changeProposalDeposit(uint256) (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/DAO.sol#1139-1146) + - DAO.executeProposal(uint256,bytes) (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/DAO.sol#853-937) + - DAO.minQuorum(uint256) (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/DAO.sol#1174-1178) + - DAO.newContract(address) (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/DAO.sol#1022-1034) + - DAO.retrieveDAOReward(bool) (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/DAO.sol#1037-1057) + - DAOInterface.rewardToken (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/DAO.sol#410) + - DAO.splitDAO(uint256,address) (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/DAO.sol#947-1020) + - closeProposal(_proposalID) (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/DAO.sol#933) + - sumOfProposalDeposits -= p.proposalDeposit (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/DAO.sol#943) + DAOInterface.sumOfProposalDeposits (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/DAO.sol#436) can be used in cross function reentrancies: + - DAO.actualBalance() (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/DAO.sol#1169-1171) + - DAO.closeProposal(uint256) (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/DAO.sol#940-945) + - DAO.newProposal(address,uint256,string,bytes,uint256,bool) (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/DAO.sol#741-806) + - DAO.splitDAO(uint256,address) (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/DAO.sol#947-1020) + - totalRewardToken += p.amount (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/DAO.sol#929) + DAOInterface.totalRewardToken (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/DAO.sol#412) can be used in cross function reentrancies: + - DAO.executeProposal(uint256,bytes) (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/DAO.sol#853-937) + - DAO.isRecipientAllowed(address) (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/DAO.sol#1158-1167) + - DAO.retrieveDAOReward(bool) (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/DAO.sol#1037-1057) + - DAOInterface.totalRewardToken (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/DAO.sol#412) + diff --git a/tests/e2e/detectors/snapshots/detectors__detector_ReentrancyEthDF_0_4_25_reentrancy_indirect_sol__0.txt b/tests/e2e/detectors/snapshots/detectors__detector_ReentrancyEthDF_0_4_25_reentrancy_indirect_sol__0.txt new file mode 100644 index 0000000000..4562e1bd7d --- /dev/null +++ b/tests/e2e/detectors/snapshots/detectors__detector_ReentrancyEthDF_0_4_25_reentrancy_indirect_sol__0.txt @@ -0,0 +1,15 @@ +Reentrancy in Reentrancy.withdraw(address) (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/reentrancy_indirect.sol#22-29): + External calls: + - require(bool)(Token(token).transfer(msg.sender,token_deposed[token][msg.sender])) (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/reentrancy_indirect.sol#24) + External calls sending eth: + - msg.sender.transfer(eth_deposed[token][msg.sender]) (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/reentrancy_indirect.sol#23) + State variables written after the call(s): + - eth_deposed[token][msg.sender] = 0 (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/reentrancy_indirect.sol#26) + Reentrancy.eth_deposed (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/reentrancy_indirect.sol#10) can be used in cross function reentrancies: + - Reentrancy.deposit_eth(address) (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/reentrancy_indirect.sol#13-15) + - Reentrancy.withdraw(address) (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/reentrancy_indirect.sol#22-29) + - token_deposed[token][msg.sender] = 0 (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/reentrancy_indirect.sol#27) + Reentrancy.token_deposed (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/reentrancy_indirect.sol#11) can be used in cross function reentrancies: + - Reentrancy.deposit_token(address,uint256) (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/reentrancy_indirect.sol#17-20) + - Reentrancy.withdraw(address) (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/reentrancy_indirect.sol#22-29) + diff --git a/tests/e2e/detectors/snapshots/detectors__detector_ReentrancyEthDF_0_4_25_reentrancy_sol__0.txt b/tests/e2e/detectors/snapshots/detectors__detector_ReentrancyEthDF_0_4_25_reentrancy_sol__0.txt new file mode 100644 index 0000000000..81143018c6 --- /dev/null +++ b/tests/e2e/detectors/snapshots/detectors__detector_ReentrancyEthDF_0_4_25_reentrancy_sol__0.txt @@ -0,0 +1,32 @@ +Reentrancy in Reentrancy.withdrawBalance_nested() (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/reentrancy.sol#74-80): + External calls: + - msg.sender.call.value(amount / 2)() (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/reentrancy.sol#77) + State variables written after the call(s): + - userBalance[msg.sender] = 0 (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/reentrancy.sol#78) + Reentrancy.userBalance (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/reentrancy.sol#4) can be used in cross function reentrancies: + - Reentrancy.addToBalance() (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/reentrancy.sol#10-12) + - Reentrancy.constructor() (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/reentrancy.sol#15-22) + - Reentrancy.getBalance(address) (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/reentrancy.sol#6-8) + - Reentrancy.withdrawBalance() (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/reentrancy.sol#24-31) + - Reentrancy.withdrawBalance_fixed() (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/reentrancy.sol#33-41) + - Reentrancy.withdrawBalance_fixed_2() (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/reentrancy.sol#43-50) + - Reentrancy.withdrawBalance_fixed_3() (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/reentrancy.sol#52-60) + - Reentrancy.withdrawBalance_fixed_4() (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/reentrancy.sol#61-72) + - Reentrancy.withdrawBalance_nested() (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/reentrancy.sol#74-80) + +Reentrancy in Reentrancy.withdrawBalance() (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/reentrancy.sol#24-31): + External calls: + - ! (msg.sender.call.value(userBalance[msg.sender])()) (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/reentrancy.sol#27) + State variables written after the call(s): + - userBalance[msg.sender] = 0 (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/reentrancy.sol#30) + Reentrancy.userBalance (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/reentrancy.sol#4) can be used in cross function reentrancies: + - Reentrancy.addToBalance() (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/reentrancy.sol#10-12) + - Reentrancy.constructor() (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/reentrancy.sol#15-22) + - Reentrancy.getBalance(address) (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/reentrancy.sol#6-8) + - Reentrancy.withdrawBalance() (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/reentrancy.sol#24-31) + - Reentrancy.withdrawBalance_fixed() (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/reentrancy.sol#33-41) + - Reentrancy.withdrawBalance_fixed_2() (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/reentrancy.sol#43-50) + - Reentrancy.withdrawBalance_fixed_3() (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/reentrancy.sol#52-60) + - Reentrancy.withdrawBalance_fixed_4() (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/reentrancy.sol#61-72) + - Reentrancy.withdrawBalance_nested() (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/reentrancy.sol#74-80) + diff --git a/tests/e2e/detectors/snapshots/detectors__detector_ReentrancyEthDF_0_5_16_reentrancy_indirect_sol__0.txt b/tests/e2e/detectors/snapshots/detectors__detector_ReentrancyEthDF_0_5_16_reentrancy_indirect_sol__0.txt new file mode 100644 index 0000000000..eea2c47116 --- /dev/null +++ b/tests/e2e/detectors/snapshots/detectors__detector_ReentrancyEthDF_0_5_16_reentrancy_indirect_sol__0.txt @@ -0,0 +1,15 @@ +Reentrancy in Reentrancy.withdraw(address) (tests/e2e/detectors/test_data/reentrancy-eth/0.5.16/reentrancy_indirect.sol#22-29): + External calls: + - require(bool)(Token(token).transfer(msg.sender,token_deposed[token][msg.sender])) (tests/e2e/detectors/test_data/reentrancy-eth/0.5.16/reentrancy_indirect.sol#24) + External calls sending eth: + - msg.sender.transfer(eth_deposed[token][msg.sender]) (tests/e2e/detectors/test_data/reentrancy-eth/0.5.16/reentrancy_indirect.sol#23) + State variables written after the call(s): + - eth_deposed[token][msg.sender] = 0 (tests/e2e/detectors/test_data/reentrancy-eth/0.5.16/reentrancy_indirect.sol#26) + Reentrancy.eth_deposed (tests/e2e/detectors/test_data/reentrancy-eth/0.5.16/reentrancy_indirect.sol#10) can be used in cross function reentrancies: + - Reentrancy.deposit_eth(address) (tests/e2e/detectors/test_data/reentrancy-eth/0.5.16/reentrancy_indirect.sol#13-15) + - Reentrancy.withdraw(address) (tests/e2e/detectors/test_data/reentrancy-eth/0.5.16/reentrancy_indirect.sol#22-29) + - token_deposed[token][msg.sender] = 0 (tests/e2e/detectors/test_data/reentrancy-eth/0.5.16/reentrancy_indirect.sol#27) + Reentrancy.token_deposed (tests/e2e/detectors/test_data/reentrancy-eth/0.5.16/reentrancy_indirect.sol#11) can be used in cross function reentrancies: + - Reentrancy.deposit_token(address,uint256) (tests/e2e/detectors/test_data/reentrancy-eth/0.5.16/reentrancy_indirect.sol#17-20) + - Reentrancy.withdraw(address) (tests/e2e/detectors/test_data/reentrancy-eth/0.5.16/reentrancy_indirect.sol#22-29) + diff --git a/tests/e2e/detectors/snapshots/detectors__detector_ReentrancyEthDF_0_5_16_reentrancy_sol__0.txt b/tests/e2e/detectors/snapshots/detectors__detector_ReentrancyEthDF_0_5_16_reentrancy_sol__0.txt new file mode 100644 index 0000000000..d77726404c --- /dev/null +++ b/tests/e2e/detectors/snapshots/detectors__detector_ReentrancyEthDF_0_5_16_reentrancy_sol__0.txt @@ -0,0 +1,28 @@ +Reentrancy in Reentrancy.withdrawBalance() (tests/e2e/detectors/test_data/reentrancy-eth/0.5.16/reentrancy.sol#25-33): + External calls: + - (ret,mem) = msg.sender.call.value(userBalance[msg.sender])() (tests/e2e/detectors/test_data/reentrancy-eth/0.5.16/reentrancy.sol#28) + State variables written after the call(s): + - userBalance[msg.sender] = 0 (tests/e2e/detectors/test_data/reentrancy-eth/0.5.16/reentrancy.sol#32) + Reentrancy.userBalance (tests/e2e/detectors/test_data/reentrancy-eth/0.5.16/reentrancy.sol#4) can be used in cross function reentrancies: + - Reentrancy.addToBalance() (tests/e2e/detectors/test_data/reentrancy-eth/0.5.16/reentrancy.sol#10-12) + - Reentrancy.constructor() (tests/e2e/detectors/test_data/reentrancy-eth/0.5.16/reentrancy.sol#15-23) + - Reentrancy.getBalance(address) (tests/e2e/detectors/test_data/reentrancy-eth/0.5.16/reentrancy.sol#6-8) + - Reentrancy.withdrawBalance() (tests/e2e/detectors/test_data/reentrancy-eth/0.5.16/reentrancy.sol#25-33) + - Reentrancy.withdrawBalance_fixed() (tests/e2e/detectors/test_data/reentrancy-eth/0.5.16/reentrancy.sol#35-44) + - Reentrancy.withdrawBalance_fixed_2() (tests/e2e/detectors/test_data/reentrancy-eth/0.5.16/reentrancy.sol#46-53) + - Reentrancy.withdrawBalance_fixed_3() (tests/e2e/detectors/test_data/reentrancy-eth/0.5.16/reentrancy.sol#55-64) + +Reentrancy in Reentrancy.withdrawBalance_fixed_3() (tests/e2e/detectors/test_data/reentrancy-eth/0.5.16/reentrancy.sol#55-64): + External calls: + - (ret,mem) = msg.sender.call.value(amount)() (tests/e2e/detectors/test_data/reentrancy-eth/0.5.16/reentrancy.sol#60) + State variables written after the call(s): + - userBalance[msg.sender] = amount (tests/e2e/detectors/test_data/reentrancy-eth/0.5.16/reentrancy.sol#62) + Reentrancy.userBalance (tests/e2e/detectors/test_data/reentrancy-eth/0.5.16/reentrancy.sol#4) can be used in cross function reentrancies: + - Reentrancy.addToBalance() (tests/e2e/detectors/test_data/reentrancy-eth/0.5.16/reentrancy.sol#10-12) + - Reentrancy.constructor() (tests/e2e/detectors/test_data/reentrancy-eth/0.5.16/reentrancy.sol#15-23) + - Reentrancy.getBalance(address) (tests/e2e/detectors/test_data/reentrancy-eth/0.5.16/reentrancy.sol#6-8) + - Reentrancy.withdrawBalance() (tests/e2e/detectors/test_data/reentrancy-eth/0.5.16/reentrancy.sol#25-33) + - Reentrancy.withdrawBalance_fixed() (tests/e2e/detectors/test_data/reentrancy-eth/0.5.16/reentrancy.sol#35-44) + - Reentrancy.withdrawBalance_fixed_2() (tests/e2e/detectors/test_data/reentrancy-eth/0.5.16/reentrancy.sol#46-53) + - Reentrancy.withdrawBalance_fixed_3() (tests/e2e/detectors/test_data/reentrancy-eth/0.5.16/reentrancy.sol#55-64) + diff --git a/tests/e2e/detectors/snapshots/detectors__detector_ReentrancyEthDF_0_6_11_reentrancy_indirect_sol__0.txt b/tests/e2e/detectors/snapshots/detectors__detector_ReentrancyEthDF_0_6_11_reentrancy_indirect_sol__0.txt new file mode 100644 index 0000000000..c4cda5f345 --- /dev/null +++ b/tests/e2e/detectors/snapshots/detectors__detector_ReentrancyEthDF_0_6_11_reentrancy_indirect_sol__0.txt @@ -0,0 +1,15 @@ +Reentrancy in Reentrancy.withdraw(address) (tests/e2e/detectors/test_data/reentrancy-eth/0.6.11/reentrancy_indirect.sol#22-29): + External calls: + - require(bool)(Token(token).transfer(msg.sender,token_deposed[token][msg.sender])) (tests/e2e/detectors/test_data/reentrancy-eth/0.6.11/reentrancy_indirect.sol#24) + External calls sending eth: + - msg.sender.transfer(eth_deposed[token][msg.sender]) (tests/e2e/detectors/test_data/reentrancy-eth/0.6.11/reentrancy_indirect.sol#23) + State variables written after the call(s): + - eth_deposed[token][msg.sender] = 0 (tests/e2e/detectors/test_data/reentrancy-eth/0.6.11/reentrancy_indirect.sol#26) + Reentrancy.eth_deposed (tests/e2e/detectors/test_data/reentrancy-eth/0.6.11/reentrancy_indirect.sol#10) can be used in cross function reentrancies: + - Reentrancy.deposit_eth(address) (tests/e2e/detectors/test_data/reentrancy-eth/0.6.11/reentrancy_indirect.sol#13-15) + - Reentrancy.withdraw(address) (tests/e2e/detectors/test_data/reentrancy-eth/0.6.11/reentrancy_indirect.sol#22-29) + - token_deposed[token][msg.sender] = 0 (tests/e2e/detectors/test_data/reentrancy-eth/0.6.11/reentrancy_indirect.sol#27) + Reentrancy.token_deposed (tests/e2e/detectors/test_data/reentrancy-eth/0.6.11/reentrancy_indirect.sol#11) can be used in cross function reentrancies: + - Reentrancy.deposit_token(address,uint256) (tests/e2e/detectors/test_data/reentrancy-eth/0.6.11/reentrancy_indirect.sol#17-20) + - Reentrancy.withdraw(address) (tests/e2e/detectors/test_data/reentrancy-eth/0.6.11/reentrancy_indirect.sol#22-29) + diff --git a/tests/e2e/detectors/snapshots/detectors__detector_ReentrancyEthDF_0_6_11_reentrancy_sol__0.txt b/tests/e2e/detectors/snapshots/detectors__detector_ReentrancyEthDF_0_6_11_reentrancy_sol__0.txt new file mode 100644 index 0000000000..a3e4ec83d4 --- /dev/null +++ b/tests/e2e/detectors/snapshots/detectors__detector_ReentrancyEthDF_0_6_11_reentrancy_sol__0.txt @@ -0,0 +1,28 @@ +Reentrancy in Reentrancy.withdrawBalance_fixed_3() (tests/e2e/detectors/test_data/reentrancy-eth/0.6.11/reentrancy.sol#55-64): + External calls: + - (ret,mem) = msg.sender.call.value(amount)() (tests/e2e/detectors/test_data/reentrancy-eth/0.6.11/reentrancy.sol#60) + State variables written after the call(s): + - userBalance[msg.sender] = amount (tests/e2e/detectors/test_data/reentrancy-eth/0.6.11/reentrancy.sol#62) + Reentrancy.userBalance (tests/e2e/detectors/test_data/reentrancy-eth/0.6.11/reentrancy.sol#4) can be used in cross function reentrancies: + - Reentrancy.addToBalance() (tests/e2e/detectors/test_data/reentrancy-eth/0.6.11/reentrancy.sol#10-12) + - Reentrancy.constructor() (tests/e2e/detectors/test_data/reentrancy-eth/0.6.11/reentrancy.sol#15-23) + - Reentrancy.getBalance(address) (tests/e2e/detectors/test_data/reentrancy-eth/0.6.11/reentrancy.sol#6-8) + - Reentrancy.withdrawBalance() (tests/e2e/detectors/test_data/reentrancy-eth/0.6.11/reentrancy.sol#25-33) + - Reentrancy.withdrawBalance_fixed() (tests/e2e/detectors/test_data/reentrancy-eth/0.6.11/reentrancy.sol#35-44) + - Reentrancy.withdrawBalance_fixed_2() (tests/e2e/detectors/test_data/reentrancy-eth/0.6.11/reentrancy.sol#46-53) + - Reentrancy.withdrawBalance_fixed_3() (tests/e2e/detectors/test_data/reentrancy-eth/0.6.11/reentrancy.sol#55-64) + +Reentrancy in Reentrancy.withdrawBalance() (tests/e2e/detectors/test_data/reentrancy-eth/0.6.11/reentrancy.sol#25-33): + External calls: + - (ret,mem) = msg.sender.call.value(userBalance[msg.sender])() (tests/e2e/detectors/test_data/reentrancy-eth/0.6.11/reentrancy.sol#28) + State variables written after the call(s): + - userBalance[msg.sender] = 0 (tests/e2e/detectors/test_data/reentrancy-eth/0.6.11/reentrancy.sol#32) + Reentrancy.userBalance (tests/e2e/detectors/test_data/reentrancy-eth/0.6.11/reentrancy.sol#4) can be used in cross function reentrancies: + - Reentrancy.addToBalance() (tests/e2e/detectors/test_data/reentrancy-eth/0.6.11/reentrancy.sol#10-12) + - Reentrancy.constructor() (tests/e2e/detectors/test_data/reentrancy-eth/0.6.11/reentrancy.sol#15-23) + - Reentrancy.getBalance(address) (tests/e2e/detectors/test_data/reentrancy-eth/0.6.11/reentrancy.sol#6-8) + - Reentrancy.withdrawBalance() (tests/e2e/detectors/test_data/reentrancy-eth/0.6.11/reentrancy.sol#25-33) + - Reentrancy.withdrawBalance_fixed() (tests/e2e/detectors/test_data/reentrancy-eth/0.6.11/reentrancy.sol#35-44) + - Reentrancy.withdrawBalance_fixed_2() (tests/e2e/detectors/test_data/reentrancy-eth/0.6.11/reentrancy.sol#46-53) + - Reentrancy.withdrawBalance_fixed_3() (tests/e2e/detectors/test_data/reentrancy-eth/0.6.11/reentrancy.sol#55-64) + diff --git a/tests/e2e/detectors/snapshots/detectors__detector_ReentrancyEthDF_0_7_6_reentrancy_indirect_sol__0.txt b/tests/e2e/detectors/snapshots/detectors__detector_ReentrancyEthDF_0_7_6_reentrancy_indirect_sol__0.txt new file mode 100644 index 0000000000..2be2ab3bb5 --- /dev/null +++ b/tests/e2e/detectors/snapshots/detectors__detector_ReentrancyEthDF_0_7_6_reentrancy_indirect_sol__0.txt @@ -0,0 +1,15 @@ +Reentrancy in Reentrancy.withdraw(address) (tests/e2e/detectors/test_data/reentrancy-eth/0.7.6/reentrancy_indirect.sol#22-29): + External calls: + - require(bool)(Token(token).transfer(msg.sender,token_deposed[token][msg.sender])) (tests/e2e/detectors/test_data/reentrancy-eth/0.7.6/reentrancy_indirect.sol#24) + External calls sending eth: + - msg.sender.transfer(eth_deposed[token][msg.sender]) (tests/e2e/detectors/test_data/reentrancy-eth/0.7.6/reentrancy_indirect.sol#23) + State variables written after the call(s): + - eth_deposed[token][msg.sender] = 0 (tests/e2e/detectors/test_data/reentrancy-eth/0.7.6/reentrancy_indirect.sol#26) + Reentrancy.eth_deposed (tests/e2e/detectors/test_data/reentrancy-eth/0.7.6/reentrancy_indirect.sol#10) can be used in cross function reentrancies: + - Reentrancy.deposit_eth(address) (tests/e2e/detectors/test_data/reentrancy-eth/0.7.6/reentrancy_indirect.sol#13-15) + - Reentrancy.withdraw(address) (tests/e2e/detectors/test_data/reentrancy-eth/0.7.6/reentrancy_indirect.sol#22-29) + - token_deposed[token][msg.sender] = 0 (tests/e2e/detectors/test_data/reentrancy-eth/0.7.6/reentrancy_indirect.sol#27) + Reentrancy.token_deposed (tests/e2e/detectors/test_data/reentrancy-eth/0.7.6/reentrancy_indirect.sol#11) can be used in cross function reentrancies: + - Reentrancy.deposit_token(address,uint256) (tests/e2e/detectors/test_data/reentrancy-eth/0.7.6/reentrancy_indirect.sol#17-20) + - Reentrancy.withdraw(address) (tests/e2e/detectors/test_data/reentrancy-eth/0.7.6/reentrancy_indirect.sol#22-29) + diff --git a/tests/e2e/detectors/snapshots/detectors__detector_ReentrancyEthDF_0_7_6_reentrancy_sol__0.txt b/tests/e2e/detectors/snapshots/detectors__detector_ReentrancyEthDF_0_7_6_reentrancy_sol__0.txt new file mode 100644 index 0000000000..04098bd3fb --- /dev/null +++ b/tests/e2e/detectors/snapshots/detectors__detector_ReentrancyEthDF_0_7_6_reentrancy_sol__0.txt @@ -0,0 +1,28 @@ +Reentrancy in Reentrancy.withdrawBalance_fixed_3() (tests/e2e/detectors/test_data/reentrancy-eth/0.7.6/reentrancy.sol#55-64): + External calls: + - (ret,mem) = msg.sender.call{value: amount}() (tests/e2e/detectors/test_data/reentrancy-eth/0.7.6/reentrancy.sol#60) + State variables written after the call(s): + - userBalance[msg.sender] = amount (tests/e2e/detectors/test_data/reentrancy-eth/0.7.6/reentrancy.sol#62) + Reentrancy.userBalance (tests/e2e/detectors/test_data/reentrancy-eth/0.7.6/reentrancy.sol#4) can be used in cross function reentrancies: + - Reentrancy.addToBalance() (tests/e2e/detectors/test_data/reentrancy-eth/0.7.6/reentrancy.sol#10-12) + - Reentrancy.constructor() (tests/e2e/detectors/test_data/reentrancy-eth/0.7.6/reentrancy.sol#15-23) + - Reentrancy.getBalance(address) (tests/e2e/detectors/test_data/reentrancy-eth/0.7.6/reentrancy.sol#6-8) + - Reentrancy.withdrawBalance() (tests/e2e/detectors/test_data/reentrancy-eth/0.7.6/reentrancy.sol#25-33) + - Reentrancy.withdrawBalance_fixed() (tests/e2e/detectors/test_data/reentrancy-eth/0.7.6/reentrancy.sol#35-44) + - Reentrancy.withdrawBalance_fixed_2() (tests/e2e/detectors/test_data/reentrancy-eth/0.7.6/reentrancy.sol#46-53) + - Reentrancy.withdrawBalance_fixed_3() (tests/e2e/detectors/test_data/reentrancy-eth/0.7.6/reentrancy.sol#55-64) + +Reentrancy in Reentrancy.withdrawBalance() (tests/e2e/detectors/test_data/reentrancy-eth/0.7.6/reentrancy.sol#25-33): + External calls: + - (ret,mem) = msg.sender.call{value: userBalance[msg.sender]}() (tests/e2e/detectors/test_data/reentrancy-eth/0.7.6/reentrancy.sol#28) + State variables written after the call(s): + - userBalance[msg.sender] = 0 (tests/e2e/detectors/test_data/reentrancy-eth/0.7.6/reentrancy.sol#32) + Reentrancy.userBalance (tests/e2e/detectors/test_data/reentrancy-eth/0.7.6/reentrancy.sol#4) can be used in cross function reentrancies: + - Reentrancy.addToBalance() (tests/e2e/detectors/test_data/reentrancy-eth/0.7.6/reentrancy.sol#10-12) + - Reentrancy.constructor() (tests/e2e/detectors/test_data/reentrancy-eth/0.7.6/reentrancy.sol#15-23) + - Reentrancy.getBalance(address) (tests/e2e/detectors/test_data/reentrancy-eth/0.7.6/reentrancy.sol#6-8) + - Reentrancy.withdrawBalance() (tests/e2e/detectors/test_data/reentrancy-eth/0.7.6/reentrancy.sol#25-33) + - Reentrancy.withdrawBalance_fixed() (tests/e2e/detectors/test_data/reentrancy-eth/0.7.6/reentrancy.sol#35-44) + - Reentrancy.withdrawBalance_fixed_2() (tests/e2e/detectors/test_data/reentrancy-eth/0.7.6/reentrancy.sol#46-53) + - Reentrancy.withdrawBalance_fixed_3() (tests/e2e/detectors/test_data/reentrancy-eth/0.7.6/reentrancy.sol#55-64) + diff --git a/tests/e2e/detectors/snapshots/detectors__detector_ReentrancyEthDF_0_8_10_reentrancy_filtered_comments_sol__0.txt b/tests/e2e/detectors/snapshots/detectors__detector_ReentrancyEthDF_0_8_10_reentrancy_filtered_comments_sol__0.txt new file mode 100644 index 0000000000..4485754d3d --- /dev/null +++ b/tests/e2e/detectors/snapshots/detectors__detector_ReentrancyEthDF_0_8_10_reentrancy_filtered_comments_sol__0.txt @@ -0,0 +1,9 @@ +Reentrancy in TestWithBug.withdraw(uint256) (tests/e2e/detectors/test_data/reentrancy-eth/0.8.10/reentrancy_filtered_comments.sol#8-12): + External calls: + - Receiver(msg.sender).send_funds{value: amount}() (tests/e2e/detectors/test_data/reentrancy-eth/0.8.10/reentrancy_filtered_comments.sol#10) + State variables written after the call(s): + - balances[msg.sender] -= amount (tests/e2e/detectors/test_data/reentrancy-eth/0.8.10/reentrancy_filtered_comments.sol#11) + TestWithBug.balances (tests/e2e/detectors/test_data/reentrancy-eth/0.8.10/reentrancy_filtered_comments.sol#6) can be used in cross function reentrancies: + - TestWithBug.withdraw(uint256) (tests/e2e/detectors/test_data/reentrancy-eth/0.8.10/reentrancy_filtered_comments.sol#8-12) + - TestWithBug.withdrawFiltered(uint256) (tests/e2e/detectors/test_data/reentrancy-eth/0.8.10/reentrancy_filtered_comments.sol#15-19) + diff --git a/tests/e2e/detectors/snapshots/detectors__detector_ReentrancyEthDF_0_8_10_reentrancy_with_non_reentrant_sol__0.txt b/tests/e2e/detectors/snapshots/detectors__detector_ReentrancyEthDF_0_8_10_reentrancy_with_non_reentrant_sol__0.txt new file mode 100644 index 0000000000..ab510d0e54 --- /dev/null +++ b/tests/e2e/detectors/snapshots/detectors__detector_ReentrancyEthDF_0_8_10_reentrancy_with_non_reentrant_sol__0.txt @@ -0,0 +1,32 @@ +Reentrancy in TestWithBugInternal.withdraw_internal(uint256) (tests/e2e/detectors/test_data/reentrancy-eth/0.8.10/reentrancy_with_non_reentrant.sol#62-66): + External calls: + - Receiver(msg.sender).send_funds{value: amount}() (tests/e2e/detectors/test_data/reentrancy-eth/0.8.10/reentrancy_with_non_reentrant.sol#64) + State variables written after the call(s): + - balances[msg.sender] -= amount (tests/e2e/detectors/test_data/reentrancy-eth/0.8.10/reentrancy_with_non_reentrant.sol#65) + TestWithBugInternal.balances (tests/e2e/detectors/test_data/reentrancy-eth/0.8.10/reentrancy_with_non_reentrant.sol#52) can be used in cross function reentrancies: + - TestWithBugInternal.withdraw_all_internal() (tests/e2e/detectors/test_data/reentrancy-eth/0.8.10/reentrancy_with_non_reentrant.sol#72-76) + +Reentrancy in TestWithBug.withdraw(uint256) (tests/e2e/detectors/test_data/reentrancy-eth/0.8.10/reentrancy_with_non_reentrant.sol#13-17): + External calls: + - Receiver(msg.sender).send_funds{value: amount}() (tests/e2e/detectors/test_data/reentrancy-eth/0.8.10/reentrancy_with_non_reentrant.sol#15) + State variables written after the call(s): + - balances[msg.sender] -= amount (tests/e2e/detectors/test_data/reentrancy-eth/0.8.10/reentrancy_with_non_reentrant.sol#16) + TestWithBug.balances (tests/e2e/detectors/test_data/reentrancy-eth/0.8.10/reentrancy_with_non_reentrant.sol#7) can be used in cross function reentrancies: + - TestWithBug.withdraw_all() (tests/e2e/detectors/test_data/reentrancy-eth/0.8.10/reentrancy_with_non_reentrant.sol#19-23) + +Reentrancy in TestBugWithPublicVariable.withdraw_internal(uint256) (tests/e2e/detectors/test_data/reentrancy-eth/0.8.10/reentrancy_with_non_reentrant.sol#122-126): + External calls: + - Receiver(msg.sender).send_funds{value: amount}() (tests/e2e/detectors/test_data/reentrancy-eth/0.8.10/reentrancy_with_non_reentrant.sol#124) + State variables written after the call(s): + - balances[msg.sender] -= amount (tests/e2e/detectors/test_data/reentrancy-eth/0.8.10/reentrancy_with_non_reentrant.sol#125) + TestBugWithPublicVariable.balances (tests/e2e/detectors/test_data/reentrancy-eth/0.8.10/reentrancy_with_non_reentrant.sol#112) can be used in cross function reentrancies: + - TestBugWithPublicVariable.balances (tests/e2e/detectors/test_data/reentrancy-eth/0.8.10/reentrancy_with_non_reentrant.sol#112) + +Reentrancy in TestWithBugNonReentrantRead.withdraw(uint256) (tests/e2e/detectors/test_data/reentrancy-eth/0.8.10/reentrancy_with_non_reentrant.sol#138-142): + External calls: + - Receiver(msg.sender).send_funds{value: amount}() (tests/e2e/detectors/test_data/reentrancy-eth/0.8.10/reentrancy_with_non_reentrant.sol#140) + State variables written after the call(s): + - balances[msg.sender] -= amount (tests/e2e/detectors/test_data/reentrancy-eth/0.8.10/reentrancy_with_non_reentrant.sol#141) + TestWithBugNonReentrantRead.balances (tests/e2e/detectors/test_data/reentrancy-eth/0.8.10/reentrancy_with_non_reentrant.sol#132) can be used in cross function reentrancies: + - TestWithBugNonReentrantRead.read() (tests/e2e/detectors/test_data/reentrancy-eth/0.8.10/reentrancy_with_non_reentrant.sol#146-149) + diff --git a/tests/e2e/detectors/test_data/reentrancy-eth-df/0.4.25/DAO.sol b/tests/e2e/detectors/test_data/reentrancy-eth-df/0.4.25/DAO.sol new file mode 100644 index 0000000000..6f03a624ca --- /dev/null +++ b/tests/e2e/detectors/test_data/reentrancy-eth-df/0.4.25/DAO.sol @@ -0,0 +1,1242 @@ + +// +// Modified version to be compiler with sol 0.4 +// +/* + +- Bytecode Verification performed was compared on second iteration - + +This file is part of the DAO. + +The DAO is free software: you can redistribute it and/or modify +it under the terms of the GNU lesser General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +The DAO is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU lesser General Public License for more details. + +You should have received a copy of the GNU lesser General Public License +along with the DAO. If not, see . +*/ + + +/* +Basic, standardized Token contract with no "premine". Defines the functions to +check token balances, send tokens, send tokens on behalf of a 3rd party and the +corresponding approval process. Tokens need to be created by a derived +contract (e.g. TokenCreation.sol). + +Thank you ConsenSys, this contract originated from: +https://github.com/ConsenSys/Tokens/blob/master/Token_Contracts/contracts/Standard_Token.sol +Which is itself based on the Ethereum standardized contract APIs: +https://github.com/ethereum/wiki/wiki/Standardized_Contract_APIs +*/ + +/// @title Standard Token Contract. + +contract TokenInterface { + mapping (address => uint256) balances; + mapping (address => mapping (address => uint256)) allowed; + + /// Total amount of tokens + uint256 public totalSupply; + + /// @param _owner The address from which the balance will be retrieved + /// @return The balance + function balanceOf(address _owner) constant returns (uint256 balance); + + /// @notice Send `_amount` tokens to `_to` from `msg.sender` + /// @param _to The address of the recipient + /// @param _amount The amount of tokens to be transferred + /// @return Whether the transfer was successful or not + function transfer(address _to, uint256 _amount) returns (bool success); + + /// @notice Send `_amount` tokens to `_to` from `_from` on the condition it + /// is approved by `_from` + /// @param _from The address of the origin of the transfer + /// @param _to The address of the recipient + /// @param _amount The amount of tokens to be transferred + /// @return Whether the transfer was successful or not + function transferFrom(address _from, address _to, uint256 _amount) returns (bool success); + + /// @notice `msg.sender` approves `_spender` to spend `_amount` tokens on + /// its behalf + /// @param _spender The address of the account able to transfer the tokens + /// @param _amount The amount of tokens to be approved for transfer + /// @return Whether the approval was successful or not + function approve(address _spender, uint256 _amount) returns (bool success); + + /// @param _owner The address of the account owning tokens + /// @param _spender The address of the account able to transfer the tokens + /// @return Amount of remaining tokens of _owner that _spender is allowed + /// to spend + function allowance( + address _owner, + address _spender + ) constant returns (uint256 remaining); + + event Transfer(address indexed _from, address indexed _to, uint256 _amount); + event Approval( + address indexed _owner, + address indexed _spender, + uint256 _amount + ); +} + + +contract Token is TokenInterface { + // Protects users by preventing the execution of method calls that + // inadvertently also transferred ether + modifier noEther() {if (msg.value > 0) throw; _;} + + function balanceOf(address _owner) constant returns (uint256 balance) { + return balances[_owner]; + } + + function transfer(address _to, uint256 _amount) noEther returns (bool success) { + if (balances[msg.sender] >= _amount && _amount > 0) { + balances[msg.sender] -= _amount; + balances[_to] += _amount; + Transfer(msg.sender, _to, _amount); + return true; + } else { + return false; + } + } + + function transferFrom( + address _from, + address _to, + uint256 _amount + ) noEther returns (bool success) { + + if (balances[_from] >= _amount + && allowed[_from][msg.sender] >= _amount + && _amount > 0) { + + balances[_to] += _amount; + balances[_from] -= _amount; + allowed[_from][msg.sender] -= _amount; + Transfer(_from, _to, _amount); + return true; + } else { + return false; + } + } + + function approve(address _spender, uint256 _amount) returns (bool success) { + allowed[msg.sender][_spender] = _amount; + Approval(msg.sender, _spender, _amount); + return true; + } + + function allowance(address _owner, address _spender) constant returns (uint256 remaining) { + return allowed[_owner][_spender]; + } +} + + +/* +This file is part of the DAO. + +The DAO is free software: you can redistribute it and/or modify +it under the terms of the GNU lesser General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +The DAO is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU lesser General Public License for more details. + +You should have received a copy of the GNU lesser General Public License +along with the DAO. If not, see . +*/ + + +/* +Basic account, used by the DAO contract to separately manage both the rewards +and the extraBalance accounts. +*/ + +contract ManagedAccountInterface { + // The only address with permission to withdraw from this account + address public owner; + // If true, only the owner of the account can receive ether from it + bool public payOwnerOnly; + // The sum of ether (in wei) which has been sent to this contract + uint public accumulatedInput; + + /// @notice Sends `_amount` of wei to _recipient + /// @param _amount The amount of wei to send to `_recipient` + /// @param _recipient The address to receive `_amount` of wei + /// @return True if the send completed + function payOut(address _recipient, uint _amount) returns (bool); + + event PayOut(address indexed _recipient, uint _amount); +} + + +contract ManagedAccount is ManagedAccountInterface{ + + // The constructor sets the owner of the account + function ManagedAccount(address _owner, bool _payOwnerOnly) { + owner = _owner; + payOwnerOnly = _payOwnerOnly; + } + + // When the contract receives a transaction without data this is called. + // It counts the amount of ether it receives and stores it in + // accumulatedInput. + function() { + accumulatedInput += msg.value; + } + + function payOut(address _recipient, uint _amount) returns (bool) { + if (msg.sender != owner || msg.value > 0 || (payOwnerOnly && _recipient != owner)) + throw; + if (_recipient.call.value(_amount)()) { + PayOut(_recipient, _amount); + return true; + } else { + return false; + } + } +} +/* +This file is part of the DAO. + +The DAO is free software: you can redistribute it and/or modify +it under the terms of the GNU lesser General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +The DAO is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU lesser General Public License for more details. + +You should have received a copy of the GNU lesser General Public License +along with the DAO. If not, see . +*/ + + +/* + * Token Creation contract, used by the DAO to create its tokens and initialize + * its ether. Feel free to modify the divisor method to implement different + * Token Creation parameters +*/ + + +contract TokenCreationInterface { + + // End of token creation, in Unix time + uint public closingTime; + // Minimum fueling goal of the token creation, denominated in tokens to + // be created + uint public minTokensToCreate; + // True if the DAO reached its minimum fueling goal, false otherwise + bool public isFueled; + // For DAO splits - if privateCreation is 0, then it is a public token + // creation, otherwise only the address stored in privateCreation is + // allowed to create tokens + address public privateCreation; + // hold extra ether which has been sent after the DAO token + // creation rate has increased + ManagedAccount public extraBalance; + // tracks the amount of wei given from each contributor (used for refund) + mapping (address => uint256) weiGiven; + + /// @dev Constructor setting the minimum fueling goal and the + /// end of the Token Creation + /// @param _minTokensToCreate Minimum fueling goal in number of + /// Tokens to be created + /// @param _closingTime Date (in Unix time) of the end of the Token Creation + /// @param _privateCreation Zero means that the creation is public. A + /// non-zero address represents the only address that can create Tokens + /// (the address can also create Tokens on behalf of other accounts) + // This is the constructor: it can not be overloaded so it is commented out + // function TokenCreation( + // uint _minTokensTocreate, + // uint _closingTime, + // address _privateCreation + // ); + + /// @notice Create Token with `_tokenHolder` as the initial owner of the Token + /// @param _tokenHolder The address of the Tokens's recipient + /// @return Whether the token creation was successful + function createTokenProxy(address _tokenHolder) payable returns (bool success); + + /// @notice Refund `msg.sender` in the case the Token Creation did + /// not reach its minimum fueling goal + function refund(); + + /// @return The divisor used to calculate the token creation rate during + /// the creation phase + function divisor() constant returns (uint divisor); + + event FuelingToDate(uint value); + event CreatedToken(address indexed to, uint amount); + event Refund(address indexed to, uint value); +} + + +contract TokenCreation is TokenCreationInterface, Token { + function TokenCreation( + uint _minTokensToCreate, + uint _closingTime, + address _privateCreation) { + + closingTime = _closingTime; + minTokensToCreate = _minTokensToCreate; + privateCreation = _privateCreation; + extraBalance = new ManagedAccount(address(this), true); + } + + function createTokenProxy(address _tokenHolder) payable returns (bool success) { + if (now < closingTime && msg.value > 0 + && (privateCreation == 0 || privateCreation == msg.sender)) { + + uint token = (msg.value * 20) / divisor(); + extraBalance.call.value(msg.value - token)(); + balances[_tokenHolder] += token; + totalSupply += token; + weiGiven[_tokenHolder] += msg.value; + CreatedToken(_tokenHolder, token); + if (totalSupply >= minTokensToCreate && !isFueled) { + isFueled = true; + FuelingToDate(totalSupply); + } + return true; + } + throw; + } + + function refund() noEther { + if (now > closingTime && !isFueled) { + // Get extraBalance - will only succeed when called for the first time + if (extraBalance.balance >= extraBalance.accumulatedInput()) + extraBalance.payOut(address(this), extraBalance.accumulatedInput()); + + // Execute refund + if (msg.sender.call.value(weiGiven[msg.sender])()) { + Refund(msg.sender, weiGiven[msg.sender]); + totalSupply -= balances[msg.sender]; + balances[msg.sender] = 0; + weiGiven[msg.sender] = 0; + } + } + } + + function divisor() constant returns (uint divisor) { + // The number of (base unit) tokens per wei is calculated + // as `msg.value` * 20 / `divisor` + // The fueling period starts with a 1:1 ratio + if (closingTime - 2 weeks > now) { + return 20; + // Followed by 10 days with a daily creation rate increase of 5% + } else if (closingTime - 4 days > now) { + return (20 + (now - (closingTime - 2 weeks)) / (1 days)); + // The last 4 days there is a constant creation rate ratio of 1:1.5 + } else { + return 30; + } + } +} +/* +This file is part of the DAO. + +The DAO is free software: you can redistribute it and/or modify +it under the terms of the GNU lesser General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +The DAO is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU lesser General Public License for more details. + +You should have received a copy of the GNU lesser General Public License +along with the DAO. If not, see . +*/ + + +/* +Standard smart contract for a Decentralized Autonomous Organization (DAO) +to automate organizational governance and decision-making. +*/ + + +contract DAOInterface { + + // The amount of days for which people who try to participate in the + // creation by calling the fallback function will still get their ether back + uint constant creationGracePeriod = 40 days; + // The minimum debate period that a generic proposal can have + uint constant minProposalDebatePeriod = 2 weeks; + // The minimum debate period that a split proposal can have + uint constant minSplitDebatePeriod = 1 weeks; + // Period of days inside which it's possible to execute a DAO split + uint constant splitExecutionPeriod = 27 days; + // Period of time after which the minimum Quorum is halved + uint constant quorumHalvingPeriod = 25 weeks; + // Period after which a proposal is closed + // (used in the case `executeProposal` fails because it throws) + uint constant executeProposalPeriod = 10 days; + // Denotes the maximum proposal deposit that can be given. It is given as + // a fraction of total Ether spent plus balance of the DAO + uint constant maxDepositDivisor = 100; + + // Proposals to spend the DAO's ether or to choose a new Curator + Proposal[] public proposals; + // The quorum needed for each proposal is partially calculated by + // totalSupply / minQuorumDivisor + uint public minQuorumDivisor; + // The unix time of the last time quorum was reached on a proposal + uint public lastTimeMinQuorumMet; + + // Address of the curator + address public curator; + // The whitelist: List of addresses the DAO is allowed to send ether to + mapping (address => bool) public allowedRecipients; + + // Tracks the addresses that own Reward Tokens. Those addresses can only be + // DAOs that have split from the original DAO. Conceptually, Reward Tokens + // represent the proportion of the rewards that the DAO has the right to + // receive. These Reward Tokens are generated when the DAO spends ether. + mapping (address => uint) public rewardToken; + // Total supply of rewardToken + uint public totalRewardToken; + + // The account used to manage the rewards which are to be distributed to the + // DAO Token Holders of this DAO + ManagedAccount public rewardAccount; + + // The account used to manage the rewards which are to be distributed to + // any DAO that holds Reward Tokens + ManagedAccount public DAOrewardAccount; + + // Amount of rewards (in wei) already paid out to a certain DAO + mapping (address => uint) public DAOpaidOut; + + // Amount of rewards (in wei) already paid out to a certain address + mapping (address => uint) public paidOut; + // Map of addresses blocked during a vote (not allowed to transfer DAO + // tokens). The address points to the proposal ID. + mapping (address => uint) public blocked; + + // The minimum deposit (in wei) required to submit any proposal that is not + // requesting a new Curator (no deposit is required for splits) + uint public proposalDeposit; + + // the accumulated sum of all current proposal deposits + uint sumOfProposalDeposits; + + // Contract that is able to create a new DAO (with the same code as + // this one), used for splits + DAO_Creator public daoCreator; + + // A proposal with `newCurator == false` represents a transaction + // to be issued by this DAO + // A proposal with `newCurator == true` represents a DAO split + struct Proposal { + // The address where the `amount` will go to if the proposal is accepted + // or if `newCurator` is true, the proposed Curator of + // the new DAO). + address recipient; + // The amount to transfer to `recipient` if the proposal is accepted. + uint amount; + // A plain text description of the proposal + string description; + // A unix timestamp, denoting the end of the voting period + uint votingDeadline; + // True if the proposal's votes have yet to be counted, otherwise False + bool open; + // True if quorum has been reached, the votes have been counted, and + // the majority said yes + bool proposalPassed; + // A hash to check validity of a proposal + bytes32 proposalHash; + // Deposit in wei the creator added when submitting their proposal. It + // is taken from the msg.value of a newProposal call. + uint proposalDeposit; + // True if this proposal is to assign a new Curator + bool newCurator; + // Data needed for splitting the DAO + + SplitData[][] splitData2; + SplitData[] splitData; + // Number of Tokens in favor of the proposal + uint yea; + // Number of Tokens opposed to the proposal + uint nay; + // Simple mapping to check if a shareholder has voted for it + mapping (address => bool) votedYes; + // Simple mapping to check if a shareholder has voted against it + mapping (address => bool) votedNo; + // Address of the shareholder who created the proposal + address creator; + } + + // Used only in the case of a newCurator proposal. + struct SplitData { + // The balance of the current DAO minus the deposit at the time of split + uint splitBalance; + // The total amount of DAO Tokens in existence at the time of split. + uint totalSupply; + // Amount of Reward Tokens owned by the DAO at the time of split. + uint rewardToken; + // The new DAO contract created at the time of split. + DAO newDAO; + } + + // Used to restrict access to certain functions to only DAO Token Holders + modifier onlyTokenholders {_;} + + /// @dev Constructor setting the Curator and the address + /// for the contract able to create another DAO as well as the parameters + /// for the DAO Token Creation + /// @param _curator The Curator + /// @param _daoCreator The contract able to (re)create this DAO + /// @param _proposalDeposit The deposit to be paid for a regular proposal + /// @param _minTokensToCreate Minimum required wei-equivalent tokens + /// to be created for a successful DAO Token Creation + /// @param _closingTime Date (in Unix time) of the end of the DAO Token Creation + /// @param _privateCreation If zero the DAO Token Creation is open to public, a + /// non-zero address means that the DAO Token Creation is only for the address + // This is the constructor: it can not be overloaded so it is commented out + // function DAO( + // address _curator, + // DAO_Creator _daoCreator, + // uint _proposalDeposit, + // uint _minTokensToCreate, + // uint _closingTime, + // address _privateCreation + // ); + + /// @notice Create Token with `msg.sender` as the beneficiary + /// @return Whether the token creation was successful + function (); + + + /// @dev This function is used to send ether back + /// to the DAO, it can also be used to receive payments that should not be + /// counted as rewards (donations, grants, etc.) + /// @return Whether the DAO received the ether successfully + function receiveEther() returns(bool); + + /// @notice `msg.sender` creates a proposal to send `_amount` Wei to + /// `_recipient` with the transaction data `_transactionData`. If + /// `_newCurator` is true, then this is a proposal that splits the + /// DAO and sets `_recipient` as the new DAO's Curator. + /// @param _recipient Address of the recipient of the proposed transaction + /// @param _amount Amount of wei to be sent with the proposed transaction + /// @param _description String describing the proposal + /// @param _transactionData Data of the proposed transaction + /// @param _debatingPeriod Time used for debating a proposal, at least 2 + /// weeks for a regular proposal, 10 days for new Curator proposal + /// @param _newCurator Bool defining whether this proposal is about + /// a new Curator or not + /// @return The proposal ID. Needed for voting on the proposal + function newProposal( + address _recipient, + uint _amount, + string _description, + bytes _transactionData, + uint _debatingPeriod, + bool _newCurator + ) onlyTokenholders returns (uint _proposalID); + + /// @notice Check that the proposal with the ID `_proposalID` matches the + /// transaction which sends `_amount` with data `_transactionData` + /// to `_recipient` + /// @param _proposalID The proposal ID + /// @param _recipient The recipient of the proposed transaction + /// @param _amount The amount of wei to be sent in the proposed transaction + /// @param _transactionData The data of the proposed transaction + /// @return Whether the proposal ID matches the transaction data or not + function checkProposalCode( + uint _proposalID, + address _recipient, + uint _amount, + bytes _transactionData + ) constant returns (bool _codeChecksOut); + + /// @notice Vote on proposal `_proposalID` with `_supportsProposal` + /// @param _proposalID The proposal ID + /// @param _supportsProposal Yes/No - support of the proposal + /// @return The vote ID. + function vote( + uint _proposalID, + bool _supportsProposal + ) onlyTokenholders returns (uint _voteID); + + /// @notice Checks whether proposal `_proposalID` with transaction data + /// `_transactionData` has been voted for or rejected, and executes the + /// transaction in the case it has been voted for. + /// @param _proposalID The proposal ID + /// @param _transactionData The data of the proposed transaction + /// @return Whether the proposed transaction has been executed or not + function executeProposal( + uint _proposalID, + bytes _transactionData + ) returns (bool _success); + + /// @notice ATTENTION! I confirm to move my remaining ether to a new DAO + /// with `_newCurator` as the new Curator, as has been + /// proposed in proposal `_proposalID`. This will burn my tokens. This can + /// not be undone and will split the DAO into two DAO's, with two + /// different underlying tokens. + /// @param _proposalID The proposal ID + /// @param _newCurator The new Curator of the new DAO + /// @dev This function, when called for the first time for this proposal, + /// will create a new DAO and send the sender's portion of the remaining + /// ether and Reward Tokens to the new DAO. It will also burn the DAO Tokens + /// of the sender. + function splitDAO( + uint _proposalID, + address _newCurator + ) returns (bool _success); + + /// @dev can only be called by the DAO itself through a proposal + /// updates the contract of the DAO by sending all ether and rewardTokens + /// to the new DAO. The new DAO needs to be approved by the Curator + /// @param _newContract the address of the new contract + function newContract(address _newContract); + + + /// @notice Add a new possible recipient `_recipient` to the whitelist so + /// that the DAO can send transactions to them (using proposals) + /// @param _recipient New recipient address + /// @dev Can only be called by the current Curator + /// @return Whether successful or not + function changeAllowedRecipients(address _recipient, bool _allowed) external returns (bool _success); + + + /// @notice Change the minimum deposit required to submit a proposal + /// @param _proposalDeposit The new proposal deposit + /// @dev Can only be called by this DAO (through proposals with the + /// recipient being this DAO itself) + function changeProposalDeposit(uint _proposalDeposit) external; + + /// @notice Move rewards from the DAORewards managed account + /// @param _toMembers If true rewards are moved to the actual reward account + /// for the DAO. If not then it's moved to the DAO itself + /// @return Whether the call was successful + function retrieveDAOReward(bool _toMembers) external returns (bool _success); + + /// @notice Get my portion of the reward that was sent to `rewardAccount` + /// @return Whether the call was successful + function getMyReward() returns(bool _success); + + /// @notice Withdraw `_account`'s portion of the reward from `rewardAccount` + /// to `_account`'s balance + /// @return Whether the call was successful + function withdrawRewardFor(address _account) internal returns (bool _success); + + /// @notice Send `_amount` tokens to `_to` from `msg.sender`. Prior to this + /// getMyReward() is called. + /// @param _to The address of the recipient + /// @param _amount The amount of tokens to be transfered + /// @return Whether the transfer was successful or not + function transferWithoutReward(address _to, uint256 _amount) returns (bool success); + + /// @notice Send `_amount` tokens to `_to` from `_from` on the condition it + /// is approved by `_from`. Prior to this getMyReward() is called. + /// @param _from The address of the sender + /// @param _to The address of the recipient + /// @param _amount The amount of tokens to be transfered + /// @return Whether the transfer was successful or not + function transferFromWithoutReward( + address _from, + address _to, + uint256 _amount + ) returns (bool success); + + /// @notice Doubles the 'minQuorumDivisor' in the case quorum has not been + /// achieved in 52 weeks + /// @return Whether the change was successful or not + function halveMinQuorum() returns (bool _success); + + /// @return total number of proposals ever created + function numberOfProposals() constant returns (uint _numberOfProposals); + + /// @param _proposalID Id of the new curator proposal + /// @return Address of the new DAO + function getNewDAOAddress(uint _proposalID) constant returns (address _newDAO); + + /// @param _account The address of the account which is checked. + /// @return Whether the account is blocked (not allowed to transfer tokens) or not. + function isBlocked(address _account) internal returns (bool); + + /// @notice If the caller is blocked by a proposal whose voting deadline + /// has exprired then unblock him. + /// @return Whether the account is blocked (not allowed to transfer tokens) or not. + function unblockMe() returns (bool); + + event ProposalAdded( + uint indexed proposalID, + address recipient, + uint amount, + bool newCurator, + string description + ); + event Voted(uint indexed proposalID, bool position, address indexed voter); + event ProposalTallied(uint indexed proposalID, bool result, uint quorum); + event NewCurator(address indexed _newCurator); + event AllowedRecipientChanged(address indexed _recipient, bool _allowed); +} + +// The DAO contract itself +contract DAO is DAOInterface, Token, TokenCreation { + + // Modifier that allows only shareholders to vote and create new proposals + modifier onlyTokenholders { + if (balanceOf(msg.sender) == 0) throw; + _; + } + + function DAO( + address _curator, + DAO_Creator _daoCreator, + uint _proposalDeposit, + uint _minTokensToCreate, + uint _closingTime, + address _privateCreation + ) TokenCreation(_minTokensToCreate, _closingTime, _privateCreation) { + + curator = _curator; + daoCreator = _daoCreator; + proposalDeposit = _proposalDeposit; + rewardAccount = new ManagedAccount(address(this), false); + DAOrewardAccount = new ManagedAccount(address(this), false); + if (address(rewardAccount) == 0) + throw; + if (address(DAOrewardAccount) == 0) + throw; + lastTimeMinQuorumMet = now; + minQuorumDivisor = 5; // sets the minimal quorum to 20% + proposals.length = 1; // avoids a proposal with ID 0 because it is used + + allowedRecipients[address(this)] = true; + allowedRecipients[curator] = true; + } + + function () { +/* if (now < closingTime + creationGracePeriod && msg.sender != address(extraBalance)) + return createTokenProxy(msg.sender); + else + return receiveEther();*/ + } + + + function receiveEther() returns (bool) { + return true; + } + + + function newProposal( + address _recipient, + uint _amount, + string _description, + bytes _transactionData, + uint _debatingPeriod, + bool _newCurator + ) onlyTokenholders returns (uint _proposalID) { + + // Sanity check + if (_newCurator && ( + _amount != 0 + || _transactionData.length != 0 + || _recipient == curator + || msg.value > 0 + || _debatingPeriod < minSplitDebatePeriod)) { + throw; + } else if ( + !_newCurator + && (!isRecipientAllowed(_recipient) || (_debatingPeriod < minProposalDebatePeriod)) + ) { + throw; + } + + if (_debatingPeriod > 8 weeks) + throw; + + if (!isFueled + || now < closingTime + || (msg.value < proposalDeposit && !_newCurator)) { + + throw; + } + + if (now + _debatingPeriod < now) // prevents overflow + throw; + + // to prevent a 51% attacker to convert the ether into deposit + if (msg.sender == address(this)) + throw; + + _proposalID = proposals.length++; + Proposal p = proposals[_proposalID]; + p.recipient = _recipient; + p.amount = _amount; + p.description = _description; + p.proposalHash = sha3(_recipient, _amount, _transactionData); + p.votingDeadline = now + _debatingPeriod; + p.open = true; + //p.proposalPassed = False; // that's default + p.newCurator = _newCurator; + if (_newCurator) + p.splitData.length++; + p.creator = msg.sender; + p.proposalDeposit = msg.value; + + sumOfProposalDeposits += msg.value; + + ProposalAdded( + _proposalID, + _recipient, + _amount, + _newCurator, + _description + ); + } + + + function checkProposalCode( + uint _proposalID, + address _recipient, + uint _amount, + bytes _transactionData + ) noEther constant returns (bool _codeChecksOut) { + Proposal p = proposals[_proposalID]; + return p.proposalHash == sha3(_recipient, _amount, _transactionData); + } + + + function vote( + uint _proposalID, + bool _supportsProposal + ) onlyTokenholders noEther returns (uint _voteID) { + + Proposal p = proposals[_proposalID]; + if (p.votedYes[msg.sender] + || p.votedNo[msg.sender] + || now >= p.votingDeadline) { + + throw; + } + + if (_supportsProposal) { + p.yea += balances[msg.sender]; + p.votedYes[msg.sender] = true; + } else { + p.nay += balances[msg.sender]; + p.votedNo[msg.sender] = true; + } + + if (blocked[msg.sender] == 0) { + blocked[msg.sender] = _proposalID; + } else if (p.votingDeadline > proposals[blocked[msg.sender]].votingDeadline) { + // this proposal's voting deadline is further into the future than + // the proposal that blocks the sender so make it the blocker + blocked[msg.sender] = _proposalID; + } + + Voted(_proposalID, _supportsProposal, msg.sender); + } + + + function executeProposal( + uint _proposalID, + bytes _transactionData + ) noEther returns (bool _success) { + + Proposal p = proposals[_proposalID]; + + uint waitPeriod = p.newCurator + ? splitExecutionPeriod + : executeProposalPeriod; + // If we are over deadline and waiting period, assert proposal is closed + if (p.open && now > p.votingDeadline + waitPeriod) { + closeProposal(_proposalID); + return; + } + + // Check if the proposal can be executed + if (now < p.votingDeadline // has the voting deadline arrived? + // Have the votes been counted? + || !p.open + // Does the transaction code match the proposal? + || p.proposalHash != sha3(p.recipient, p.amount, _transactionData)) { + + throw; + } + + // If the curator removed the recipient from the whitelist, close the proposal + // in order to free the deposit and allow unblocking of voters + if (!isRecipientAllowed(p.recipient)) { + closeProposal(_proposalID); + p.creator.send(p.proposalDeposit); + return; + } + + bool proposalCheck = true; + + if (p.amount > actualBalance()) + proposalCheck = false; + + uint quorum = p.yea + p.nay; + + // require 53% for calling newContract() + if (_transactionData.length >= 4 && _transactionData[0] == 0x68 + && _transactionData[1] == 0x37 && _transactionData[2] == 0xff + && _transactionData[3] == 0x1e + && quorum < minQuorum(actualBalance() + rewardToken[address(this)])) { + + proposalCheck = false; + } + + if (quorum >= minQuorum(p.amount)) { + if (!p.creator.send(p.proposalDeposit)) + throw; + + lastTimeMinQuorumMet = now; + // set the minQuorum to 20% again, in the case it has been reached + if (quorum > totalSupply / 5) + minQuorumDivisor = 5; + } + + // Execute result + if (quorum >= minQuorum(p.amount) && p.yea > p.nay && proposalCheck) { + if (!p.recipient.call.value(p.amount)(_transactionData)) + throw; + + p.proposalPassed = true; + _success = true; + + // only create reward tokens when ether is not sent to the DAO itself and + // related addresses. Proxy addresses should be forbidden by the curator. + if (p.recipient != address(this) && p.recipient != address(rewardAccount) + && p.recipient != address(DAOrewardAccount) + && p.recipient != address(extraBalance) + && p.recipient != address(curator)) { + + rewardToken[address(this)] += p.amount; + totalRewardToken += p.amount; + } + } + + closeProposal(_proposalID); + + // Initiate event + ProposalTallied(_proposalID, _success, quorum); + } + + + function closeProposal(uint _proposalID) internal { + Proposal p = proposals[_proposalID]; + if (p.open) + sumOfProposalDeposits -= p.proposalDeposit; + p.open = false; + } + + function splitDAO( + uint _proposalID, + address _newCurator + ) noEther onlyTokenholders returns (bool _success) { + + Proposal p = proposals[_proposalID]; + + // Sanity check + + if (now < p.votingDeadline // has the voting deadline arrived? + //The request for a split expires XX days after the voting deadline + || now > p.votingDeadline + splitExecutionPeriod + // Does the new Curator address match? + || p.recipient != _newCurator + // Is it a new curator proposal? + || !p.newCurator + // Have you voted for this split? + || !p.votedYes[msg.sender] + // Did you already vote on another proposal? + || (blocked[msg.sender] != _proposalID && blocked[msg.sender] != 0) ) { + + throw; + } + + // If the new DAO doesn't exist yet, create the new DAO and store the + // current split data + if (address(p.splitData[0].newDAO) == 0) { + p.splitData[0].newDAO = createNewDAO(_newCurator); + // Call depth limit reached, etc. + if (address(p.splitData[0].newDAO) == 0) + throw; + // should never happen + if (this.balance < sumOfProposalDeposits) + throw; + p.splitData[0].splitBalance = actualBalance(); + p.splitData[0].rewardToken = rewardToken[address(this)]; + p.splitData[0].totalSupply = totalSupply; + p.proposalPassed = true; + } + + // Move ether and assign new Tokens + uint fundsToBeMoved = + (balances[msg.sender] * p.splitData[0].splitBalance) / + p.splitData[0].totalSupply; + if (p.splitData[0].newDAO.createTokenProxy.value(fundsToBeMoved)(msg.sender) == false) + throw; + + + // Assign reward rights to new DAO + uint rewardTokenToBeMoved = + (balances[msg.sender] * p.splitData[0].rewardToken) / + p.splitData[0].totalSupply; + + uint paidOutToBeMoved = DAOpaidOut[address(this)] * rewardTokenToBeMoved / + rewardToken[address(this)]; + + rewardToken[address(p.splitData[0].newDAO)] += rewardTokenToBeMoved; + if (rewardToken[address(this)] < rewardTokenToBeMoved) + throw; + rewardToken[address(this)] -= rewardTokenToBeMoved; + + DAOpaidOut[address(p.splitData[0].newDAO)] += paidOutToBeMoved; + if (DAOpaidOut[address(this)] < paidOutToBeMoved) + throw; + DAOpaidOut[address(this)] -= paidOutToBeMoved; + + // Burn DAO Tokens + Transfer(msg.sender, 0, balances[msg.sender]); + withdrawRewardFor(msg.sender); // be nice, and get his rewards + totalSupply -= balances[msg.sender]; + balances[msg.sender] = 0; + paidOut[msg.sender] = 0; + return true; + } + + function newContract(address _newContract){ + if (msg.sender != address(this) || !allowedRecipients[_newContract]) return; + // move all ether + if (!_newContract.call.value(address(this).balance)()) { + throw; + } + + //move all reward tokens + rewardToken[_newContract] += rewardToken[address(this)]; + rewardToken[address(this)] = 0; + DAOpaidOut[_newContract] += DAOpaidOut[address(this)]; + DAOpaidOut[address(this)] = 0; + } + + + function retrieveDAOReward(bool _toMembers) external noEther returns (bool _success) { + DAO dao = DAO(msg.sender); + + if ((rewardToken[msg.sender] * DAOrewardAccount.accumulatedInput()) / + totalRewardToken < DAOpaidOut[msg.sender]) + throw; + + uint reward = + (rewardToken[msg.sender] * DAOrewardAccount.accumulatedInput()) / + totalRewardToken - DAOpaidOut[msg.sender]; + if(_toMembers) { + if (!DAOrewardAccount.payOut(dao.rewardAccount(), reward)) + throw; + } + else { + if (!DAOrewardAccount.payOut(dao, reward)) + throw; + } + DAOpaidOut[msg.sender] += reward; + return true; + } + + function getMyReward() noEther returns (bool _success) { + return withdrawRewardFor(msg.sender); + } + + + function withdrawRewardFor(address _account) noEther internal returns (bool _success) { + if ((balanceOf(_account) * rewardAccount.accumulatedInput()) / totalSupply < paidOut[_account]) + throw; + + uint reward = + (balanceOf(_account) * rewardAccount.accumulatedInput()) / totalSupply - paidOut[_account]; + if (!rewardAccount.payOut(_account, reward)) + throw; + paidOut[_account] += reward; + return true; + } + + + function transfer(address _to, uint256 _value) returns (bool success) { + if (isFueled + && now > closingTime + && !isBlocked(msg.sender) + && transferPaidOut(msg.sender, _to, _value) + && super.transfer(_to, _value)) { + + return true; + } else { + throw; + } + } + + + function transferWithoutReward(address _to, uint256 _value) returns (bool success) { + if (!getMyReward()) + throw; + return transfer(_to, _value); + } + + + function transferFrom(address _from, address _to, uint256 _value) returns (bool success) { + if (isFueled + && now > closingTime + && !isBlocked(_from) + && transferPaidOut(_from, _to, _value) + && super.transferFrom(_from, _to, _value)) { + + return true; + } else { + throw; + } + } + + + function transferFromWithoutReward( + address _from, + address _to, + uint256 _value + ) returns (bool success) { + + if (!withdrawRewardFor(_from)) + throw; + return transferFrom(_from, _to, _value); + } + + + function transferPaidOut( + address _from, + address _to, + uint256 _value + ) internal returns (bool success) { + + uint transferPaidOut = paidOut[_from] * _value / balanceOf(_from); + if (transferPaidOut > paidOut[_from]) + throw; + paidOut[_from] -= transferPaidOut; + paidOut[_to] += transferPaidOut; + return true; + } + + + function changeProposalDeposit(uint _proposalDeposit) noEther external { + if (msg.sender != address(this) || _proposalDeposit > (actualBalance() + rewardToken[address(this)]) + / maxDepositDivisor) { + + throw; + } + proposalDeposit = _proposalDeposit; + } + + + function changeAllowedRecipients(address _recipient, bool _allowed) noEther external returns (bool _success) { + if (msg.sender != curator) + throw; + allowedRecipients[_recipient] = _allowed; + AllowedRecipientChanged(_recipient, _allowed); + return true; + } + + + function isRecipientAllowed(address _recipient) internal returns (bool _isAllowed) { + if (allowedRecipients[_recipient] + || (_recipient == address(extraBalance) + // only allowed when at least the amount held in the + // extraBalance account has been spent from the DAO + && totalRewardToken > extraBalance.accumulatedInput())) + return true; + else + return false; + } + + function actualBalance() constant returns (uint _actualBalance) { + return this.balance - sumOfProposalDeposits; + } + + + function minQuorum(uint _value) internal constant returns (uint _minQuorum) { + // minimum of 20% and maximum of 53.33% + return totalSupply / minQuorumDivisor + + (_value * totalSupply) / (3 * (actualBalance() + rewardToken[address(this)])); + } + + + function halveMinQuorum() returns (bool _success) { + // this can only be called after `quorumHalvingPeriod` has passed or at anytime + // by the curator with a delay of at least `minProposalDebatePeriod` between the calls + if ((lastTimeMinQuorumMet < (now - quorumHalvingPeriod) || msg.sender == curator) + && lastTimeMinQuorumMet < (now - minProposalDebatePeriod)) { + lastTimeMinQuorumMet = now; + minQuorumDivisor *= 2; + return true; + } else { + return false; + } + } + + function createNewDAO(address _newCurator) internal returns (DAO _newDAO) { + NewCurator(_newCurator); + return daoCreator.createDAO(_newCurator, 0, 0, now + splitExecutionPeriod); + } + + function numberOfProposals() constant returns (uint _numberOfProposals) { + // Don't count index 0. It's used by isBlocked() and exists from start + return proposals.length - 1; + } + + function getNewDAOAddress(uint _proposalID) constant returns (address _newDAO) { + return proposals[_proposalID].splitData[0].newDAO; + } + + function isBlocked(address _account) internal returns (bool) { + if (blocked[_account] == 0) + return false; + Proposal p = proposals[blocked[_account]]; + if (now > p.votingDeadline) { + blocked[_account] = 0; + return false; + } else { + return true; + } + } + + function unblockMe() returns (bool) { + return isBlocked(msg.sender); + } +} + +contract DAO_Creator { + function createDAO( + address _curator, + uint _proposalDeposit, + uint _minTokensToCreate, + uint _closingTime + ) returns (DAO _newDAO) { + + return new DAO( + _curator, + DAO_Creator(this), + _proposalDeposit, + _minTokensToCreate, + _closingTime, + msg.sender + ); + } +} diff --git a/tests/e2e/detectors/test_data/reentrancy-eth-df/0.4.25/DAO.sol-0.4.25.zip b/tests/e2e/detectors/test_data/reentrancy-eth-df/0.4.25/DAO.sol-0.4.25.zip new file mode 100644 index 0000000000..5b8fb4fb7f Binary files /dev/null and b/tests/e2e/detectors/test_data/reentrancy-eth-df/0.4.25/DAO.sol-0.4.25.zip differ diff --git a/tests/e2e/detectors/test_data/reentrancy-eth-df/0.4.25/reentrancy.sol b/tests/e2e/detectors/test_data/reentrancy-eth-df/0.4.25/reentrancy.sol new file mode 100644 index 0000000000..3e407accb6 --- /dev/null +++ b/tests/e2e/detectors/test_data/reentrancy-eth-df/0.4.25/reentrancy.sol @@ -0,0 +1,100 @@ +pragma solidity ^0.4.24; + +contract Reentrancy { + mapping (address => uint) userBalance; + + function getBalance(address u) view public returns(uint){ + return userBalance[u]; + } + + function addToBalance() payable public{ + userBalance[msg.sender] += msg.value; + } + + // Should not detect reentrancy in constructor + constructor() public { + // send userBalance[msg.sender] ethers to msg.sender + // if mgs.sender is a contract, it will call its fallback function + if (!(msg.sender.call.value(userBalance[msg.sender])())) { + revert(); + } + userBalance[msg.sender] = 0; + } + + function withdrawBalance() public{ + // send userBalance[msg.sender] ethers to msg.sender + // if mgs.sender is a contract, it will call its fallback function + if( ! (msg.sender.call.value(userBalance[msg.sender])() ) ){ + revert(); + } + userBalance[msg.sender] = 0; + } + + function withdrawBalance_fixed() public{ + // To protect against re-entrancy, the state variable + // has to be change before the call + uint amount = userBalance[msg.sender]; + userBalance[msg.sender] = 0; + if( ! (msg.sender.call.value(amount)() ) ){ + revert(); + } + } + + function withdrawBalance_fixed_2() public{ + // send() and transfer() are safe against reentrancy + // they do not transfer the remaining gas + // and they give just enough gas to execute few instructions + // in the fallback function (no further call possible) + msg.sender.transfer(userBalance[msg.sender]); + userBalance[msg.sender] = 0; + } + + function withdrawBalance_fixed_3() public{ + // The state can be changed + // But it is fine, as it can only occur if the transaction fails + uint amount = userBalance[msg.sender]; + userBalance[msg.sender] = 0; + if( ! (msg.sender.call.value(amount)() ) ){ + userBalance[msg.sender] = amount; + } + } + function withdrawBalance_fixed_4() public{ + // The state can be changed + // But it is fine, as it can only occur if the transaction fails + uint amount = userBalance[msg.sender]; + userBalance[msg.sender] = 0; + if( (msg.sender.call.value(amount)() ) ){ + return; + } + else{ + userBalance[msg.sender] = amount; + } + } + + function withdrawBalance_nested() public{ + uint amount = userBalance[msg.sender]; + if( ! (msg.sender.call.value(amount/2)() ) ){ + msg.sender.call.value(amount/2)(); + userBalance[msg.sender] = 0; + } + } + +} + + +contract Called{ + function f() public; +} + +contract ReentrancyEvent { + + event E(); + + function test(Called c) public{ + + c.f(); + emit E(); + + } +} + diff --git a/tests/e2e/detectors/test_data/reentrancy-eth-df/0.4.25/reentrancy.sol-0.4.25.zip b/tests/e2e/detectors/test_data/reentrancy-eth-df/0.4.25/reentrancy.sol-0.4.25.zip new file mode 100644 index 0000000000..1fa87b48a7 Binary files /dev/null and b/tests/e2e/detectors/test_data/reentrancy-eth-df/0.4.25/reentrancy.sol-0.4.25.zip differ diff --git a/tests/e2e/detectors/test_data/reentrancy-eth-df/0.4.25/reentrancy_indirect.sol b/tests/e2e/detectors/test_data/reentrancy-eth-df/0.4.25/reentrancy_indirect.sol new file mode 100644 index 0000000000..73dd3db00b --- /dev/null +++ b/tests/e2e/detectors/test_data/reentrancy-eth-df/0.4.25/reentrancy_indirect.sol @@ -0,0 +1,31 @@ +pragma solidity ^0.4.24; + +contract Token{ + function transfer(address to, uint value) returns(bool); + function transferFrom(address from, address to, uint value) returns(bool); +} + +contract Reentrancy { + + mapping(address => mapping(address => uint)) eth_deposed; + mapping(address => mapping(address => uint)) token_deposed; + + function deposit_eth(address token) payable{ + eth_deposed[token][msg.sender] += msg.value; + } + + function deposit_token(address token, uint value){ + token_deposed[token][msg.sender] += value; + require(Token(token).transferFrom(msg.sender, address(this), value)); + } + + function withdraw(address token){ + msg.sender.transfer(eth_deposed[token][msg.sender]); + require(Token(token).transfer(msg.sender, token_deposed[token][msg.sender])); + + eth_deposed[token][msg.sender] = 0; + token_deposed[token][msg.sender] = 0; + + } + +} diff --git a/tests/e2e/detectors/test_data/reentrancy-eth-df/0.4.25/reentrancy_indirect.sol-0.4.25.zip b/tests/e2e/detectors/test_data/reentrancy-eth-df/0.4.25/reentrancy_indirect.sol-0.4.25.zip new file mode 100644 index 0000000000..ac63e710de Binary files /dev/null and b/tests/e2e/detectors/test_data/reentrancy-eth-df/0.4.25/reentrancy_indirect.sol-0.4.25.zip differ diff --git a/tests/e2e/detectors/test_data/reentrancy-eth-df/0.5.16/reentrancy.sol b/tests/e2e/detectors/test_data/reentrancy-eth-df/0.5.16/reentrancy.sol new file mode 100644 index 0000000000..7163934f8f --- /dev/null +++ b/tests/e2e/detectors/test_data/reentrancy-eth-df/0.5.16/reentrancy.sol @@ -0,0 +1,65 @@ +// pragma solidity ^0.5.0; + +contract Reentrancy { + mapping (address => uint) userBalance; + + function getBalance(address u) view public returns(uint){ + return userBalance[u]; + } + + function addToBalance() payable public{ + userBalance[msg.sender] += msg.value; + } + + // Should not detect reentrancy in constructor + constructor() public { + // send userBalance[msg.sender] ethers to msg.sender + // if mgs.sender is a contract, it will call its fallback function + (bool ret, bytes memory mem) = msg.sender.call.value(userBalance[msg.sender])(""); + if( ! ret ){ + revert(); + } + userBalance[msg.sender] = 0; + } + + function withdrawBalance() public{ + // send userBalance[msg.sender] ethers to msg.sender + // if mgs.sender is a contract, it will call its fallback function + (bool ret, bytes memory mem) = msg.sender.call.value(userBalance[msg.sender])(""); + if( ! ret ){ + revert(); + } + userBalance[msg.sender] = 0; + } + + function withdrawBalance_fixed() public{ + // To protect against re-entrancy, the state variable + // has to be change before the call + uint amount = userBalance[msg.sender]; + userBalance[msg.sender] = 0; + (bool ret, bytes memory mem) = msg.sender.call.value(amount)(""); + if( ! ret ){ + revert(); + } + } + + function withdrawBalance_fixed_2() public{ + // send() and transfer() are safe against reentrancy + // they do not transfer the remaining gas + // and they give just enough gas to execute few instructions + // in the fallback function (no further call possible) + msg.sender.transfer(userBalance[msg.sender]); + userBalance[msg.sender] = 0; + } + + function withdrawBalance_fixed_3() public{ + // The state can be changed + // But it is fine, as it can only occur if the transaction fails + uint amount = userBalance[msg.sender]; + userBalance[msg.sender] = 0; + (bool ret, bytes memory mem) = msg.sender.call.value(amount)(""); + if( ! ret ){ + userBalance[msg.sender] = amount; + } + } +} diff --git a/tests/e2e/detectors/test_data/reentrancy-eth-df/0.5.16/reentrancy.sol-0.5.16.zip b/tests/e2e/detectors/test_data/reentrancy-eth-df/0.5.16/reentrancy.sol-0.5.16.zip new file mode 100644 index 0000000000..c105dd00e9 Binary files /dev/null and b/tests/e2e/detectors/test_data/reentrancy-eth-df/0.5.16/reentrancy.sol-0.5.16.zip differ diff --git a/tests/e2e/detectors/test_data/reentrancy-eth-df/0.5.16/reentrancy_indirect.sol b/tests/e2e/detectors/test_data/reentrancy-eth-df/0.5.16/reentrancy_indirect.sol new file mode 100644 index 0000000000..b2f9ec884a --- /dev/null +++ b/tests/e2e/detectors/test_data/reentrancy-eth-df/0.5.16/reentrancy_indirect.sol @@ -0,0 +1,31 @@ +// pragma solidity ^0.4.24; + +contract Token{ + function transfer(address to, uint value) public returns(bool); + function transferFrom(address from, address to, uint value) public returns(bool); +} + +contract Reentrancy { + + mapping(address => mapping(address => uint)) eth_deposed; + mapping(address => mapping(address => uint)) token_deposed; + + function deposit_eth(address token) public payable{ + eth_deposed[token][msg.sender] += msg.value; + } + + function deposit_token(address token, uint value) public { + token_deposed[token][msg.sender] += value; + require(Token(token).transferFrom(msg.sender, address(this), value)); + } + + function withdraw(address token) public { + msg.sender.transfer(eth_deposed[token][msg.sender]); + require(Token(token).transfer(msg.sender, token_deposed[token][msg.sender])); + + eth_deposed[token][msg.sender] = 0; + token_deposed[token][msg.sender] = 0; + + } + +} diff --git a/tests/e2e/detectors/test_data/reentrancy-eth-df/0.5.16/reentrancy_indirect.sol-0.5.16.zip b/tests/e2e/detectors/test_data/reentrancy-eth-df/0.5.16/reentrancy_indirect.sol-0.5.16.zip new file mode 100644 index 0000000000..90bd7283e2 Binary files /dev/null and b/tests/e2e/detectors/test_data/reentrancy-eth-df/0.5.16/reentrancy_indirect.sol-0.5.16.zip differ diff --git a/tests/e2e/detectors/test_data/reentrancy-eth-df/0.6.11/reentrancy.sol b/tests/e2e/detectors/test_data/reentrancy-eth-df/0.6.11/reentrancy.sol new file mode 100644 index 0000000000..13843d7897 --- /dev/null +++ b/tests/e2e/detectors/test_data/reentrancy-eth-df/0.6.11/reentrancy.sol @@ -0,0 +1,65 @@ +// pragma solidity ^0.5.0; + +contract Reentrancy { + mapping (address => uint) userBalance; + + function getBalance(address u) view public returns(uint){ + return userBalance[u]; + } + + function addToBalance() payable public{ + userBalance[msg.sender] += msg.value; + } + + // Should not detect reentrancy in constructor + constructor() public { + // send userBalance[msg.sender] ethers to msg.sender + // if mgs.sender is a contract, it will call its fallback function + (bool ret, bytes memory mem) = msg.sender.call{value:userBalance[msg.sender]}(""); + if( ! ret ){ + revert(); + } + userBalance[msg.sender] = 0; + } + + function withdrawBalance() public{ + // send userBalance[msg.sender] ethers to msg.sender + // if mgs.sender is a contract, it will call its fallback function + (bool ret, bytes memory mem) = msg.sender.call.value(userBalance[msg.sender])(""); + if( ! ret ){ + revert(); + } + userBalance[msg.sender] = 0; + } + + function withdrawBalance_fixed() public{ + // To protect against re-entrancy, the state variable + // has to be change before the call + uint amount = userBalance[msg.sender]; + userBalance[msg.sender] = 0; + (bool ret, bytes memory mem) = msg.sender.call.value(amount)(""); + if( ! ret ){ + revert(); + } + } + + function withdrawBalance_fixed_2() public{ + // send() and transfer() are safe against reentrancy + // they do not transfer the remaining gas + // and they give just enough gas to execute few instructions + // in the fallback function (no further call possible) + msg.sender.transfer(userBalance[msg.sender]); + userBalance[msg.sender] = 0; + } + + function withdrawBalance_fixed_3() public{ + // The state can be changed + // But it is fine, as it can only occur if the transaction fails + uint amount = userBalance[msg.sender]; + userBalance[msg.sender] = 0; + (bool ret, bytes memory mem) = msg.sender.call.value(amount)(""); + if( ! ret ){ + userBalance[msg.sender] = amount; + } + } +} diff --git a/tests/e2e/detectors/test_data/reentrancy-eth-df/0.6.11/reentrancy.sol-0.6.11.zip b/tests/e2e/detectors/test_data/reentrancy-eth-df/0.6.11/reentrancy.sol-0.6.11.zip new file mode 100644 index 0000000000..b8ddf59eba Binary files /dev/null and b/tests/e2e/detectors/test_data/reentrancy-eth-df/0.6.11/reentrancy.sol-0.6.11.zip differ diff --git a/tests/e2e/detectors/test_data/reentrancy-eth-df/0.6.11/reentrancy_indirect.sol b/tests/e2e/detectors/test_data/reentrancy-eth-df/0.6.11/reentrancy_indirect.sol new file mode 100644 index 0000000000..d1067e362f --- /dev/null +++ b/tests/e2e/detectors/test_data/reentrancy-eth-df/0.6.11/reentrancy_indirect.sol @@ -0,0 +1,31 @@ +// pragma solidity ^0.4.24; + +abstract contract Token{ + function transfer(address to, uint value) public virtual returns(bool); + function transferFrom(address from, address to, uint value) public virtual returns(bool); +} + +contract Reentrancy { + + mapping(address => mapping(address => uint)) eth_deposed; + mapping(address => mapping(address => uint)) token_deposed; + + function deposit_eth(address token) public payable{ + eth_deposed[token][msg.sender] += msg.value; + } + + function deposit_token(address token, uint value) public { + token_deposed[token][msg.sender] += value; + require(Token(token).transferFrom(msg.sender, address(this), value)); + } + + function withdraw(address token) public { + msg.sender.transfer(eth_deposed[token][msg.sender]); + require(Token(token).transfer(msg.sender, token_deposed[token][msg.sender])); + + eth_deposed[token][msg.sender] = 0; + token_deposed[token][msg.sender] = 0; + + } + +} diff --git a/tests/e2e/detectors/test_data/reentrancy-eth-df/0.6.11/reentrancy_indirect.sol-0.6.11.zip b/tests/e2e/detectors/test_data/reentrancy-eth-df/0.6.11/reentrancy_indirect.sol-0.6.11.zip new file mode 100644 index 0000000000..5aeda8ec13 Binary files /dev/null and b/tests/e2e/detectors/test_data/reentrancy-eth-df/0.6.11/reentrancy_indirect.sol-0.6.11.zip differ diff --git a/tests/e2e/detectors/test_data/reentrancy-eth-df/0.7.6/reentrancy.sol b/tests/e2e/detectors/test_data/reentrancy-eth-df/0.7.6/reentrancy.sol new file mode 100644 index 0000000000..12af82cf94 --- /dev/null +++ b/tests/e2e/detectors/test_data/reentrancy-eth-df/0.7.6/reentrancy.sol @@ -0,0 +1,65 @@ +// pragma solidity ^0.5.0; + +contract Reentrancy { + mapping (address => uint) userBalance; + + function getBalance(address u) view public returns(uint){ + return userBalance[u]; + } + + function addToBalance() payable public{ + userBalance[msg.sender] += msg.value; + } + + // Should not detect reentrancy in constructor + constructor() public { + // send userBalance[msg.sender] ethers to msg.sender + // if mgs.sender is a contract, it will call its fallback function + (bool ret, bytes memory mem) = msg.sender.call{value:userBalance[msg.sender]}(""); + if( ! ret ){ + revert(); + } + userBalance[msg.sender] = 0; + } + + function withdrawBalance() public{ + // send userBalance[msg.sender] ethers to msg.sender + // if mgs.sender is a contract, it will call its fallback function + (bool ret, bytes memory mem) = msg.sender.call{value:userBalance[msg.sender]}(""); + if( ! ret ){ + revert(); + } + userBalance[msg.sender] = 0; + } + + function withdrawBalance_fixed() public{ + // To protect against re-entrancy, the state variable + // has to be change before the call + uint amount = userBalance[msg.sender]; + userBalance[msg.sender] = 0; + (bool ret, bytes memory mem) = msg.sender.call{value:amount}(""); + if( ! ret ){ + revert(); + } + } + + function withdrawBalance_fixed_2() public{ + // send() and transfer() are safe against reentrancy + // they do not transfer the remaining gas + // and they give just enough gas to execute few instructions + // in the fallback function (no further call possible) + msg.sender.transfer(userBalance[msg.sender]); + userBalance[msg.sender] = 0; + } + + function withdrawBalance_fixed_3() public{ + // The state can be changed + // But it is fine, as it can only occur if the transaction fails + uint amount = userBalance[msg.sender]; + userBalance[msg.sender] = 0; + (bool ret, bytes memory mem) = msg.sender.call{value:amount}(""); + if( ! ret ){ + userBalance[msg.sender] = amount; + } + } +} diff --git a/tests/e2e/detectors/test_data/reentrancy-eth-df/0.7.6/reentrancy.sol-0.7.6.zip b/tests/e2e/detectors/test_data/reentrancy-eth-df/0.7.6/reentrancy.sol-0.7.6.zip new file mode 100644 index 0000000000..24c706745b Binary files /dev/null and b/tests/e2e/detectors/test_data/reentrancy-eth-df/0.7.6/reentrancy.sol-0.7.6.zip differ diff --git a/tests/e2e/detectors/test_data/reentrancy-eth-df/0.7.6/reentrancy_indirect.sol b/tests/e2e/detectors/test_data/reentrancy-eth-df/0.7.6/reentrancy_indirect.sol new file mode 100644 index 0000000000..d1067e362f --- /dev/null +++ b/tests/e2e/detectors/test_data/reentrancy-eth-df/0.7.6/reentrancy_indirect.sol @@ -0,0 +1,31 @@ +// pragma solidity ^0.4.24; + +abstract contract Token{ + function transfer(address to, uint value) public virtual returns(bool); + function transferFrom(address from, address to, uint value) public virtual returns(bool); +} + +contract Reentrancy { + + mapping(address => mapping(address => uint)) eth_deposed; + mapping(address => mapping(address => uint)) token_deposed; + + function deposit_eth(address token) public payable{ + eth_deposed[token][msg.sender] += msg.value; + } + + function deposit_token(address token, uint value) public { + token_deposed[token][msg.sender] += value; + require(Token(token).transferFrom(msg.sender, address(this), value)); + } + + function withdraw(address token) public { + msg.sender.transfer(eth_deposed[token][msg.sender]); + require(Token(token).transfer(msg.sender, token_deposed[token][msg.sender])); + + eth_deposed[token][msg.sender] = 0; + token_deposed[token][msg.sender] = 0; + + } + +} diff --git a/tests/e2e/detectors/test_data/reentrancy-eth-df/0.7.6/reentrancy_indirect.sol-0.7.6.zip b/tests/e2e/detectors/test_data/reentrancy-eth-df/0.7.6/reentrancy_indirect.sol-0.7.6.zip new file mode 100644 index 0000000000..34ba3bcdf8 Binary files /dev/null and b/tests/e2e/detectors/test_data/reentrancy-eth-df/0.7.6/reentrancy_indirect.sol-0.7.6.zip differ diff --git a/tests/e2e/detectors/test_data/reentrancy-eth-df/0.8.10/reentrancy_filtered_comments.sol b/tests/e2e/detectors/test_data/reentrancy-eth-df/0.8.10/reentrancy_filtered_comments.sol new file mode 100644 index 0000000000..4bc02966dc --- /dev/null +++ b/tests/e2e/detectors/test_data/reentrancy-eth-df/0.8.10/reentrancy_filtered_comments.sol @@ -0,0 +1,21 @@ +interface Receiver { + function send_funds() external payable; +} + +contract TestWithBug { + mapping(address => uint) balances; + + function withdraw(uint amount) public { + require(amount <= balances[msg.sender]); + Receiver(msg.sender).send_funds{value: amount}(); + balances[msg.sender] -= amount; + } + + // // slither-disable-start all + // function withdrawFiltered(uint amount) public { + // require(amount <= balances[msg.sender]); + // Receiver(msg.sender).send_funds{value: amount}(); + // balances[msg.sender] -= amount; + // } + // // slither-disable-end all +} diff --git a/tests/e2e/detectors/test_data/reentrancy-eth-df/0.8.10/reentrancy_filtered_comments.sol-0.8.10.zip b/tests/e2e/detectors/test_data/reentrancy-eth-df/0.8.10/reentrancy_filtered_comments.sol-0.8.10.zip new file mode 100644 index 0000000000..f73bdbef23 Binary files /dev/null and b/tests/e2e/detectors/test_data/reentrancy-eth-df/0.8.10/reentrancy_filtered_comments.sol-0.8.10.zip differ diff --git a/tests/e2e/detectors/test_data/reentrancy-eth-df/0.8.10/reentrancy_with_non_reentrant.sol b/tests/e2e/detectors/test_data/reentrancy-eth-df/0.8.10/reentrancy_with_non_reentrant.sol new file mode 100644 index 0000000000..938738217e --- /dev/null +++ b/tests/e2e/detectors/test_data/reentrancy-eth-df/0.8.10/reentrancy_with_non_reentrant.sol @@ -0,0 +1,151 @@ +interface Receiver{ + function send_funds() payable external; +} + +contract TestWithBug{ + + mapping(address => uint) balances; + + modifier nonReentrant(){ + _; + } + + function withdraw(uint amount) nonReentrant public{ + require(amount <= balances[msg.sender]); + Receiver(msg.sender).send_funds{value: amount}(); + balances[msg.sender] -= amount; + } + + function withdraw_all() public{ + uint amount = balances[msg.sender]; + balances[msg.sender] = 0; + Receiver(msg.sender).send_funds{value: amount}(); + } + +} + + +contract TestWithoutBug{ + + mapping(address => uint) balances; + + modifier nonReentrant(){ + _; + } + + function withdraw(uint amount) nonReentrant public{ + require(amount <= balances[msg.sender]); + Receiver(msg.sender).send_funds{value: amount}(); + balances[msg.sender] -= amount; + } + + function withdraw_all() nonReentrant public{ + uint amount = balances[msg.sender]; + balances[msg.sender] = 0; + Receiver(msg.sender).send_funds{value: amount}(); + } + +} + +contract TestWithBugInternal{ + + mapping(address => uint) balances; + + modifier nonReentrant(){ + _; + } + + function withdraw(uint amount) nonReentrant public{ + withdraw_internal(amount); + } + + function withdraw_internal(uint amount) internal{ + require(amount <= balances[msg.sender]); + Receiver(msg.sender).send_funds{value: amount}(); + balances[msg.sender] -= amount; + } + + function withdraw_all() public{ + withdraw_all_internal(); + } + + function withdraw_all_internal() internal { + uint amount = balances[msg.sender]; + balances[msg.sender] = 0; + Receiver(msg.sender).send_funds{value: amount}(); + } + +} + +contract TestWithoutBugInternal{ + + mapping(address => uint) balances; + + modifier nonReentrant(){ + _; + } + + function withdraw(uint amount) nonReentrant public{ + withdraw_internal(amount); + } + + function withdraw_internal(uint amount) internal{ + require(amount <= balances[msg.sender]); + Receiver(msg.sender).send_funds{value: amount}(); + balances[msg.sender] -= amount; + } + + function withdraw_all() nonReentrant public{ + withdraw_all_internal(); + } + + function withdraw_all_internal() internal { + uint amount = balances[msg.sender]; + balances[msg.sender] = 0; + Receiver(msg.sender).send_funds{value: amount}(); + } + +} + +contract TestBugWithPublicVariable{ + + mapping(address => uint) public balances; + + modifier nonReentrant(){ + _; + } + + function withdraw(uint amount) nonReentrant public{ + withdraw_internal(amount); + } + + function withdraw_internal(uint amount) internal{ + require(amount <= balances[msg.sender]); + Receiver(msg.sender).send_funds{value: amount}(); + balances[msg.sender] -= amount; + } + +} + +contract TestWithBugNonReentrantRead{ + + mapping(address => uint) balances; + + modifier nonReentrant(){ + _; + } + + function withdraw(uint amount) nonReentrant public{ + require(amount <= balances[msg.sender]); + Receiver(msg.sender).send_funds{value: amount}(); + balances[msg.sender] -= amount; + } + + // Simulate a reentrancy that allows to read variable in a potential incorrect state during a reentrancy + // This is more likely to impact protocol like reentrancy + function read() public returns(uint){ + uint amount = balances[msg.sender]; + return amount; + } + +} diff --git a/tests/e2e/detectors/test_data/reentrancy-eth-df/0.8.10/reentrancy_with_non_reentrant.sol-0.8.10.zip b/tests/e2e/detectors/test_data/reentrancy-eth-df/0.8.10/reentrancy_with_non_reentrant.sol-0.8.10.zip new file mode 100644 index 0000000000..cc3cef205c Binary files /dev/null and b/tests/e2e/detectors/test_data/reentrancy-eth-df/0.8.10/reentrancy_with_non_reentrant.sol-0.8.10.zip differ diff --git a/tests/e2e/detectors/test_data/reentrancy-eth/0.8.10/reentrancy_filtered_comments.sol b/tests/e2e/detectors/test_data/reentrancy-eth/0.8.10/reentrancy_filtered_comments.sol index b036bd7fd0..4bc02966dc 100644 --- a/tests/e2e/detectors/test_data/reentrancy-eth/0.8.10/reentrancy_filtered_comments.sol +++ b/tests/e2e/detectors/test_data/reentrancy-eth/0.8.10/reentrancy_filtered_comments.sol @@ -1,22 +1,21 @@ -interface Receiver{ - function send_funds() payable external; +interface Receiver { + function send_funds() external payable; } -contract TestWithBug{ +contract TestWithBug { mapping(address => uint) balances; - function withdraw(uint amount) public{ - require(amount <= balances[msg.sender]); - Receiver(msg.sender).send_funds{value: amount}(); - balances[msg.sender] -= amount; + function withdraw(uint amount) public { + require(amount <= balances[msg.sender]); + Receiver(msg.sender).send_funds{value: amount}(); + balances[msg.sender] -= amount; } - // slither-disable-start all - function withdrawFiltered(uint amount) public{ - require(amount <= balances[msg.sender]); - Receiver(msg.sender).send_funds{value: amount}(); - balances[msg.sender] -= amount; - } - // slither-disable-end all + // // slither-disable-start all + // function withdrawFiltered(uint amount) public { + // require(amount <= balances[msg.sender]); + // Receiver(msg.sender).send_funds{value: amount}(); + // balances[msg.sender] -= amount; + // } + // // slither-disable-end all } - diff --git a/tests/e2e/detectors/test_detectors.py b/tests/e2e/detectors/test_detectors.py index 38ae278d3e..0c01ac5986 100644 --- a/tests/e2e/detectors/test_detectors.py +++ b/tests/e2e/detectors/test_detectors.py @@ -367,6 +367,52 @@ def id_test(test_item: Test): Test(all_detectors.ReentrancyEth, "reentrancy_with_non_reentrant.sol", "0.8.10"), # Test parse_ignore_comments Test(all_detectors.ReentrancyEth, "reentrancy_filtered_comments.sol", "0.8.10"), + # ReentrancyEthDF tests - data flow based reentrancy detection + Test( + all_detectors.ReentrancyEthDF, + "reentrancy.sol", + "0.4.25", + ), + Test( + all_detectors.ReentrancyEthDF, + "reentrancy_indirect.sol", + "0.4.25", + ), + Test( + all_detectors.ReentrancyEthDF, + "reentrancy.sol", + "0.5.16", + ), + Test( + all_detectors.ReentrancyEthDF, + "reentrancy_indirect.sol", + "0.5.16", + ), + Test( + all_detectors.ReentrancyEthDF, + "reentrancy.sol", + "0.6.11", + ), + Test( + all_detectors.ReentrancyEthDF, + "reentrancy_indirect.sol", + "0.6.11", + ), + Test(all_detectors.ReentrancyEthDF, "reentrancy.sol", "0.7.6"), + Test( + all_detectors.ReentrancyEthDF, + "reentrancy_indirect.sol", + "0.7.6", + ), + # Test( + # all_detectors.ReentrancyEthDF, + # "DAO.sol", + # "0.4.25", + # ), + # Test the nonReentrant filtering + Test(all_detectors.ReentrancyEthDF, "reentrancy_with_non_reentrant.sol", "0.8.10"), + # Test parse_ignore_comments + Test(all_detectors.ReentrancyEthDF, "reentrancy_filtered_comments.sol", "0.8.10"), Test( all_detectors.UninitializedStorageVars, "uninitialized_storage_pointer.sol", @@ -1945,6 +1991,7 @@ def id_test(test_item: Test): TEST_DATA_DIR = Path(__file__).resolve().parent / "test_data" + # pylint: disable=too-many-locals @pytest.mark.parametrize("test_item", ALL_TESTS, ids=id_test) def test_detector(test_item: Test, snapshot):