diff --git a/slither/detectors/all_detectors.py b/slither/detectors/all_detectors.py index ff1c352c3..f593c377e 100644 --- a/slither/detectors/all_detectors.py +++ b/slither/detectors/all_detectors.py @@ -99,3 +99,4 @@ from .statements.return_bomb import ReturnBomb from .functions.out_of_order_retryable import OutOfOrderRetryable from .statements.unused_import import UnusedImport +from .oracles.spot_price import SpotPriceDetector diff --git a/slither/detectors/oracles/spot_price.py b/slither/detectors/oracles/spot_price.py new file mode 100644 index 000000000..f5318df72 --- /dev/null +++ b/slither/detectors/oracles/spot_price.py @@ -0,0 +1,393 @@ +from typing import List +from slither.core.declarations.function_contract import FunctionContract +from slither.detectors.abstract_detector import AbstractDetector +from slither.slithir.operations import HighLevelCall, Operation, LibraryCall, Assignment +from slither.analyses.data_dependency.data_dependency import is_dependent +from slither.slithir.variables.variable import Variable +from slither.core.cfg.node import Node, recheable, NodeType +from slither.detectors.abstract_detector import DetectorClassification +from slither.slithir.operations import ( + Binary, + BinaryType, +) + +from slither.core.declarations.function import Function + + +# SpotPriceUsage class to store the node and interface +# For better readability of messages +class SpotPriceUsage: + def __init__(self, node: Node, interface: str): + self.node = node + self.interface = interface + + def mapping(self): + return self.node.source_mapping + + def type_of_interface(self): + return self.interface + + +class SpotPriceDetector(AbstractDetector): + """ + Documentation: This detector is used to detect the usage of spot price in the contract. + """ + + ARGUMENT = ( + "oracle-spot-price" # slither will launch the detector with slither.py --detect mydetector + ) + HELP = "Oracle vulnerabilities" + IMPACT = DetectorClassification.INFORMATIONAL + CONFIDENCE = DetectorClassification.INFORMATIONAL + + WIKI = "https://github.com/crytic/slither/wiki/Detector-Documentation#oracle-spot-price" + + WIKI_TITLE = "Oracle Spot prices" + WIKI_DESCRIPTION = "Detection of spot price usage" + WIKI_RECOMMENDATION = "Using spot price for calculations can lead to vulnerabilities. Make sure to validate the data before using it or consider use of TWAP oracles." + + # SafeMath functions for compatibility with solidity contracts < 0.8.0 version + SAFEMATH_FUNCTIONS = ["mul", "div"] + # Uniswap calculations functions + CALC_FUNCTIONS = ["getAmountOut", "getAmountsOut"] + # Protected functions -> Indicating TWAP V2 oracles + PROTECTED_FUNCTIONS = [ + "currentCumulativePrices", + "price1CumulativeLast", + "price0CumulativeLast", + ] + # Uniswap interfaces + UNISWAP_INTERFACES = ["IUniswapV3Pool", "IUniswapV2Pair"] + # Suspicious calls for Uniswap + UNISWAP_SUSPICIOUS_CALLS = ["slot0", "getReserves"] + + # Check if the instance of the call is made, and if the function name and interface name are the same + # Or if one of them at least matches + @staticmethod + def instance_of_call(ir: Operation, function_name, interface_name) -> bool: + if isinstance(ir, HighLevelCall): + if not hasattr(ir.function, "name") or not hasattr(ir.destination, "type"): + return False + if isinstance(ir.destination, Variable): + if function_name is not None and interface_name is not None: + if ( + str(ir.destination.type) == interface_name + and ir.function.name == function_name + ): + return True + elif function_name is None: + if str(ir.destination.type) == interface_name: + return True + elif interface_name is None: + if ir.function.name == function_name: + return True + + return False + + # Ignore functions related to calculation of TWAP + def ignore_function(self, ir) -> bool: + for function in self.PROTECTED_FUNCTIONS: + if self.instance_of_call(ir, function, None): + return True + return False + + # Get the arguments of the high level call + @staticmethod + def get_argument_of_high_level_call(ir: Operation) -> List[Variable]: + if isinstance(ir, HighLevelCall): + return ir.arguments + return [] + + # Check if the address of the destination is different + @staticmethod + def different_address(first_ir: Operation, second_ir: Operation): + if ( + hasattr(first_ir, "destination") + and hasattr(first_ir, "destination") + and first_ir.destination != second_ir.destination + ): + return True + return False + + # Detect oracle call + def detect_oracle_call( + self, function: FunctionContract, function_names, interface_names + ) -> (Node, str): + + nodes = [] + first_node = None + first_arguments = [] + swap_indicators = [] + # For finding next node + counter = 0 + for node in function.nodes: + for ir in node.irs: + for i in range(len(function_names)): # pylint: disable=consider-using-enumerate + function_name = function_names[i] + interface_name = interface_names[i] + + # Ignore functions related to calculation of TWAP + if self.ignore_function(ir): + return [] + # Detect UniswapV3 or UniswapV2 + if self.instance_of_call(ir, function_name, interface_name): + if interface_name == "IUniswapV3Pool" and not self.slot0_returned_price( + node.variables_written + ): + continue + nodes.append((node, interface_name)) + + # Detect any fork of Uniswap + elif self.instance_of_call(ir, function_name, None): + if interface_name == "IUniswapV3Pool" and not self.slot0_returned_price( + node.variables_written + ): + continue + nodes.append((node, interface_name)) + + # Swap indication + elif self.instance_of_call(ir, "swap", None): + swap_indicators = [node] + + # Detection of balanceOf spot price pattern + if self.instance_of_call(ir, "balanceOf", None): + arguments = self.get_argument_of_high_level_call(ir) + # Node need to be set and argument of the call need to be the same as the first one + if ( + first_node is not None + and arguments[0] == first_arguments[0] + and self.different_address(first_node[1], ir) + and counter == 1 + ): + nodes.append(([first_node[0], node], "BalanceOF")) + first_node = None + first_arguments = [] + + else: + first_arguments = arguments # Store arguments for comparison + first_node = ( + node, + ir, + ) # Node and ir which stores destination can be used for address var comparison + counter = 0 + break + # Counter used to check if the next node is immediately after the first node to follow pattern + counter += 1 + + return nodes, swap_indicators + + # Detect spot price usage + # 1. Detect Uniswap V3 + # 2. Detect Uniswap V2 + # 3. Detect any fork of Uniswap + # 4. Detect balanceOf method usage which can indicate spot price usage in certain cases + def detect_spot_price_usage(self): + spot_price_usage = [] + swap_functions = [] + for contract in self.contracts: + for function in contract.functions: + + oracle_calls, swap_function = self.detect_oracle_call( + function, + ["slot0", "getReserves"], + ["IUniswapV3Pool", "IUniswapV2Pair"], + ) + uniswap = False + for call in oracle_calls: + spot_price_usage.append(SpotPriceUsage(call[0], call[1])) + if call[1] in self.UNISWAP_INTERFACES: + uniswap = True + if uniswap and swap_function: + swap_functions.append(function) + return spot_price_usage, swap_functions + + # Check if arithmetic operations are made + # Compatibility with SafeMath library + def detect_arithmetic_operations(self, node: Node) -> bool: + for ir in node.irs: + if isinstance(ir, Binary): + if ir.type in ( + BinaryType.MULTIPLICATION, + BinaryType.DIVISION, + ): + return True + elif isinstance(ir, LibraryCall): + if hasattr(ir, "function"): + if ir.function.name in self.SAFEMATH_FUNCTIONS: + return True + # if arithmetic_op: + # if "FixedPoint.fraction" in str(node): + # return False + return False + + def calc_functions(self, node: Node) -> bool: + for ir in node.irs: + if isinstance(ir, HighLevelCall): + if ir.function.name in self.CALC_FUNCTIONS: + return True + return False + + # Check if slot0 returned price value + @staticmethod + def slot0_returned_price(variables) -> bool: + for var in variables: + # sqrtPricex96 is type uint160 and only var of this type is returned by slot0 + if hasattr(var, "type") and str(var.type) == "uint160": + return True + return False + + # Check getReserves vars + # reserve0 and reserve1 are of type uint112 or someone could directly cast them to uint256 + @staticmethod + def check_reserve_var(var) -> bool: + return hasattr(var, "type") and str(var.type) == "uint112" or str(var.type) == "uint256" + + # Track if the variable was assigned to different variable without change + @staticmethod + def track_var(variable, node) -> bool: + temp_variable = None + for ir in node.irs: + if isinstance(ir, Assignment): + if str(ir.rvalue) == str(variable): + temp_variable = ir.lvalue + else: + return variable + if temp_variable is not None: + for v in node.variables_written: + if str(v) == str(temp_variable): + variable = v + print(variable) + return variable + + @staticmethod + # Check if calculations are linked to return, that would indicate only get/calculation function + def are_calcs_linked_to_return(node: Node) -> bool: + function = node.function + variables = node.variables_written + returned_vars = function.returns + for r_var in returned_vars: + for var in variables: + if is_dependent(r_var, var, function): + return function + if node.type == NodeType.RETURN: + return function + for s in node.sons: + if s.type == NodeType.RETURN: + return function + return None + + # Check if calculations are made with spot data + def are_calculations_made_with_spot_data(self, node: Node, interface: str) -> Node: + + # For the case when the node is not a list, create a list + # This is done to make compatibility with balanceOf method usage which returns two nodes + if not isinstance(node, list): + node = [node] + + # Check if the node is used in calculations + nodes = [] + return_functions = [] + + # Loop because of BalanceOF method usage as it returns two nodes + while node: + variables = node[0].variables_written + recheable_nodes = recheable(node[0]) + changed_vars = [] + # Track the variable if it was assigned to different variable without change + for n in recheable_nodes: + for var in variables: + changed_vars.append(self.track_var(var, n)) + # Check if the variable is used in arithmetic operations or calculate function + for n in recheable_nodes: + for var in changed_vars: + if var in n.variables_read: + if interface == "IUniswapV2Pair" and not self.check_reserve_var(var): + continue + # Check if the variable is used in arithmetic operations + if self.detect_arithmetic_operations(n): + nodes.append(n) + # Check if the variable is used in calculation functions + elif self.calc_functions(n): + nodes.append(n) + node.pop() + # Check if the spot price data are returned + for node2 in nodes: + function = self.are_calcs_linked_to_return(node2) + return_functions.append(function) + return nodes, return_functions + + # Check if the function, where the spot price data is obtained is used anywhere + @staticmethod + def only_return(function: Function) -> bool: + if function is None: + return False + + if (function.view or function.pure) and not function.reachable_from_functions: + return True + + return False + + # Generate informative messages for the detected spot price usage + @staticmethod + def generate_informative_messages(spot_price_classes, swap_functions): + messages = [] + additional_message = "" + # Iterate through all spot price method occuriences + for spot_price in spot_price_classes: + if not isinstance(spot_price.node, list): + node = [spot_price.node] + else: + node = spot_price.node + # Check if the function is in the swap functions + # Statement add for informative purposes + for function in swap_functions: + if node[0].function == function: + additional_message = " inside function where performed swap operation" + if spot_price.interface == "IUniswapV3Pool": + messages.append( + f"Method which could indicate usage of spot price was detected in Uniswap V3 at {spot_price.node.source_mapping}{additional_message}\n{spot_price.node}\n" + ) + elif spot_price.interface == "IUniswapV2Pair": + messages.append( + f"Method which could indicate usage of spot price was detected in Uniswap V2 at {spot_price.node.source_mapping}{additional_message}\n{spot_price.node}\n" + ) + elif spot_price.interface is None: + messages.append( + f"Method which could indicate usage of spot price was detected in Uniswap Fork at {spot_price.node.source_mapping}{additional_message}\n{spot_price.node}\n" + ) + elif spot_price.interface == "BalanceOF": + messages.append( + f"Method which could indicate usage of spot price was detected at {spot_price.node[0].source_mapping} and {spot_price.node[1].source_mapping}.\n{spot_price.node[0]}\n{spot_price.node[1]}\n" + ) + additional_message = "" + return messages + + # Generate message for the node which occured in calculations + @staticmethod + def generate_calc_messages(node: Node, only_return: bool) -> str: + if only_return: + return f"Calculations are made with spot price data in {node.source_mapping} but the function is not used anywhere in the contract.\n" + + return f"Calculations are made with spot price data in {node.source_mapping}\n" + + def _detect(self): + results = [] + spot_price_usage, swap_functions = self.detect_spot_price_usage() + if spot_price_usage: + messages = self.generate_informative_messages(spot_price_usage, swap_functions) + + for spot_price in spot_price_usage: + nodes, return_functions = self.are_calculations_made_with_spot_data( + spot_price.node, spot_price.interface + ) + if nodes: + for i in range(len(nodes)): # pylint: disable=consider-using-enumerate + only_return = self.only_return(return_functions[i]) + messages.append(self.generate_calc_messages(nodes[i], only_return)) + + # It can contain duplication, sorted and unique messages. + # Sorting due to testing purposes + messages = sorted(list(set(messages))) + res = self.generate_result(messages) + results.append(res) + return results diff --git a/slither/slithir/convert.py b/slither/slithir/convert.py index 7d8aa543b..b9ec0f7e4 100644 --- a/slither/slithir/convert.py +++ b/slither/slithir/convert.py @@ -1584,6 +1584,18 @@ def convert_to_library_or_top_level( if new_ir: return new_ir + if ( + isinstance(t, ElementaryType) + and t.name == "address" + and ir.destination.name == "this" + and UserDefinedType(node.function.contract) in using_for + ): + new_ir = look_for_library_or_top_level( + contract, ir, using_for, UserDefinedType(node.function.contract) + ) + if new_ir: + return new_ir + return None diff --git a/tests/e2e/detectors/snapshots/detectors__detector_SpotPriceDetector_0_6_12_spot_price_getReserves_sol__0.txt b/tests/e2e/detectors/snapshots/detectors__detector_SpotPriceDetector_0_6_12_spot_price_getReserves_sol__0.txt new file mode 100644 index 000000000..fbe5b772e --- /dev/null +++ b/tests/e2e/detectors/snapshots/detectors__detector_SpotPriceDetector_0_6_12_spot_price_getReserves_sol__0.txt @@ -0,0 +1,4 @@ +Calculations are made with spot price data in tests/e2e/detectors/test_data/oracle-spot-price/0.6.12/spot_price_getReserves.sol#200 +Method which could indicate usage of spot price was detected in Uniswap V2 at tests/e2e/detectors/test_data/oracle-spot-price/0.6.12/spot_price_getReserves.sol#197 +EXPRESSION (_yaxReserves) = yaxisEthUniswapV2Pair.getReserves() + diff --git a/tests/e2e/detectors/snapshots/detectors__detector_SpotPriceDetector_0_8_20_spot_price_balanceOf_sol__0.txt b/tests/e2e/detectors/snapshots/detectors__detector_SpotPriceDetector_0_8_20_spot_price_balanceOf_sol__0.txt new file mode 100644 index 000000000..4e62bfe1a --- /dev/null +++ b/tests/e2e/detectors/snapshots/detectors__detector_SpotPriceDetector_0_8_20_spot_price_balanceOf_sol__0.txt @@ -0,0 +1,5 @@ +Calculations are made with spot price data in tests/e2e/detectors/test_data/oracle-spot-price/0.8.20/spot_price_balanceOf.sol#41 but the function is not used anywhere in the contract. +Method which could indicate usage of spot price was detected at tests/e2e/detectors/test_data/oracle-spot-price/0.8.20/spot_price_balanceOf.sol#38 and tests/e2e/detectors/test_data/oracle-spot-price/0.8.20/spot_price_balanceOf.sol#39. +NEW VARIABLE usdcBalance = IERC20(USDCAddress).balanceOf(pool) +NEW VARIABLE ethBalance = IERC20(weth).balanceOf(pool) + diff --git a/tests/e2e/detectors/snapshots/detectors__detector_SpotPriceDetector_0_8_20_spot_price_getAmountOut_sol__0.txt b/tests/e2e/detectors/snapshots/detectors__detector_SpotPriceDetector_0_8_20_spot_price_getAmountOut_sol__0.txt new file mode 100644 index 000000000..7c9cb5d22 --- /dev/null +++ b/tests/e2e/detectors/snapshots/detectors__detector_SpotPriceDetector_0_8_20_spot_price_getAmountOut_sol__0.txt @@ -0,0 +1,4 @@ +Calculations are made with spot price data in tests/e2e/detectors/test_data/oracle-spot-price/0.8.20/spot_price_getAmountOut.sol#66-71 but the function is not used anywhere in the contract. +Method which could indicate usage of spot price was detected in Uniswap V2 at tests/e2e/detectors/test_data/oracle-spot-price/0.8.20/spot_price_getAmountOut.sol#59 +EXPRESSION (left,right) = pair.getReserves() + diff --git a/tests/e2e/detectors/test_data/oracle-spot-price/0.6.12/spot_price_getReserves.sol b/tests/e2e/detectors/test_data/oracle-spot-price/0.6.12/spot_price_getReserves.sol new file mode 100644 index 000000000..8680e5258 --- /dev/null +++ b/tests/e2e/detectors/test_data/oracle-spot-price/0.6.12/spot_price_getReserves.sol @@ -0,0 +1,218 @@ +// Resource: https://solodit.xyz/issues/m-10-yaxisvotepowerbalanceof-can-be-manipulated-code4rena-yaxis-yaxis-contest-git + +// SPDX-License-Identifier: MIT +pragma solidity 0.6.12; + +library SafeMath { + /** + * @dev Multiplies two numbers, throws on overflow. + */ + function mul(uint256 a, uint256 b) internal pure returns (uint256 c) { + if (a == 0) { + return 0; + } + c = a * b; + assert(c / a == b); + return c; + } + + /** + * @dev Integer division of two numbers, truncating the quotient. + */ + function div(uint256 a, uint256 b) internal pure returns (uint256) { + // assert(b > 0); // Solidity automatically throws when dividing by 0 + // uint256 c = a / b; + // assert(a == b * c + a % b); // There is no case in which this doesn't hold + return a / b; + } + + /** + * @dev Subtracts two numbers, throws on overflow (i.e. if subtrahend is greater than minuend). + */ + function sub(uint256 a, uint256 b) internal pure returns (uint256) { + assert(b <= a); + return a - b; + } + + /** + * @dev Adds two numbers, throws on overflow. + */ + function add(uint256 a, uint256 b) internal pure returns (uint256 c) { + c = a + b; + assert(c >= a); + return c; + } +} + +interface IRewards { + function balanceOf(address) external view returns (uint256); + function earned(address) external view returns (uint256); + function totalSupply() external view returns (uint256); +} +interface IUniswapV2Pair { + event Approval(address indexed owner, address indexed spender, uint value); + event Transfer(address indexed from, address indexed to, uint value); + + function name() external pure returns (string memory); + function symbol() external pure returns (string memory); + function decimals() external pure returns (uint8); + function totalSupply() external view returns (uint); + function balanceOf(address owner) external view returns (uint); + function allowance( + address owner, + address spender + ) external view returns (uint); + + function approve(address spender, uint value) external returns (bool); + function transfer(address to, uint value) external returns (bool); + function transferFrom( + address from, + address to, + uint value + ) external returns (bool); + + function DOMAIN_SEPARATOR() external view returns (bytes32); + function PERMIT_TYPEHASH() external pure returns (bytes32); + function nonces(address owner) external view returns (uint); + + function permit( + address owner, + address spender, + uint value, + uint deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external; + + event Mint(address indexed sender, uint amount0, uint amount1); + event Burn( + address indexed sender, + uint amount0, + uint amount1, + address indexed to + ); + event Swap( + address indexed sender, + uint amount0In, + uint amount1In, + uint amount0Out, + uint amount1Out, + address indexed to + ); + event Sync(uint112 reserve0, uint112 reserve1); + + function MINIMUM_LIQUIDITY() external pure returns (uint); + function factory() external view returns (address); + function token0() external view returns (address); + function token1() external view returns (address); + function getReserves() + external + view + returns (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast); + function price0CumulativeLast() external view returns (uint); + function price1CumulativeLast() external view returns (uint); + function kLast() external view returns (uint); + + function mint(address to) external returns (uint liquidity); + function burn(address to) external returns (uint amount0, uint amount1); + function swap( + uint amount0Out, + uint amount1Out, + address to, + bytes calldata data + ) external; + function skim(address to) external; + function sync() external; + + function initialize(address, address) external; +} + +interface IVoteProxy { + function decimals() external pure returns (uint8); + function totalSupply() external view returns (uint256); + function balanceOf(address _voter) external view returns (uint256); +} + +interface IERC20 { + event Transfer(address indexed from, address indexed to, uint256 value); + + event Approval( + address indexed owner, + address indexed spender, + uint256 value + ); + + function totalSupply() external view returns (uint256); + + function balanceOf(address account) external view returns (uint256); + + function transfer(address to, uint256 value) external returns (bool); + + function allowance( + address owner, + address spender + ) external view returns (uint256); + + function approve(address spender, uint256 value) external returns (bool); + + function transferFrom( + address from, + address to, + uint256 value + ) external returns (bool); +} + +contract YaxisVotePower is IVoteProxy { + using SafeMath for uint256; + + // solhint-disable-next-line const-name-snakecase + uint8 public constant override decimals = uint8(18); + + IUniswapV2Pair public immutable yaxisEthUniswapV2Pair; + IERC20 public immutable yaxis; + IRewards public immutable rewardsYaxis; + IRewards public immutable rewardsYaxisEth; + + constructor( + address _yaxis, + address _rewardsYaxis, + address _rewardsYaxisEth, + address _yaxisEthUniswapV2Pair + ) public { + yaxis = IERC20(_yaxis); + rewardsYaxis = IRewards(_rewardsYaxis); + rewardsYaxisEth = IRewards(_rewardsYaxisEth); + yaxisEthUniswapV2Pair = IUniswapV2Pair(_yaxisEthUniswapV2Pair); + } + + function totalSupply() external view override returns (uint256) { + return sqrt(yaxis.totalSupply()); + } + + function balanceOf( + address _voter + ) external view override returns (uint256 _balance) { + uint256 _stakeAmount = rewardsYaxisEth.balanceOf(_voter); + (uint256 _yaxReserves, , ) = yaxisEthUniswapV2Pair.getReserves(); + uint256 _supply = yaxisEthUniswapV2Pair.totalSupply(); + _supply = _supply == 0 ? 1e18 : _supply; + uint256 _lpStakingYax = _yaxReserves.mul(_stakeAmount).div(_supply); + uint256 _rewardsYaxisAmount = rewardsYaxis.balanceOf(_voter).add( + rewardsYaxis.earned(_voter) + ); + _balance = sqrt( + yaxis.balanceOf(_voter).add(_lpStakingYax).add(_rewardsYaxisAmount) + ); + } + + function sqrt(uint256 x) private pure returns (uint256 y) { + uint256 z = (x + 1) / 2; + y = x; + while (z < y) { + y = z; + z = (x / z + z) / 2; + } + y = y * (10 ** 9); + } +} diff --git a/tests/e2e/detectors/test_data/oracle-spot-price/0.6.12/spot_price_getReserves.sol-0.6.12.zip b/tests/e2e/detectors/test_data/oracle-spot-price/0.6.12/spot_price_getReserves.sol-0.6.12.zip new file mode 100644 index 000000000..f9c65c83b Binary files /dev/null and b/tests/e2e/detectors/test_data/oracle-spot-price/0.6.12/spot_price_getReserves.sol-0.6.12.zip differ diff --git a/tests/e2e/detectors/test_data/oracle-spot-price/0.8.20/spot_price_balanceOf.sol b/tests/e2e/detectors/test_data/oracle-spot-price/0.8.20/spot_price_balanceOf.sol new file mode 100644 index 000000000..80d4754ca --- /dev/null +++ b/tests/e2e/detectors/test_data/oracle-spot-price/0.8.20/spot_price_balanceOf.sol @@ -0,0 +1,43 @@ +pragma solidity 0.8.20; + +interface IERC20 { + event Transfer(address indexed from, address indexed to, uint256 value); + + event Approval( + address indexed owner, + address indexed spender, + uint256 value + ); + + function totalSupply() external view returns (uint256); + + function balanceOf(address account) external view returns (uint256); + + function transfer(address to, uint256 value) external returns (bool); + + function allowance( + address owner, + address spender + ) external view returns (uint256); + + function approve(address spender, uint256 value) external returns (bool); + + function transferFrom( + address from, + address to, + uint256 value + ) external returns (bool); +} + +contract BalanceOfData { + function getPriceV2( + address pool, + address USDCAddress, + address weth + ) internal view returns (uint256 price) { + uint256 usdcBalance = IERC20(USDCAddress).balanceOf(pool); + uint256 ethBalance = IERC20(weth).balanceOf(pool); + + price = (ethBalance * 10 ** 18) / usdcBalance; + } +} diff --git a/tests/e2e/detectors/test_data/oracle-spot-price/0.8.20/spot_price_balanceOf.sol-0.8.20.zip b/tests/e2e/detectors/test_data/oracle-spot-price/0.8.20/spot_price_balanceOf.sol-0.8.20.zip new file mode 100644 index 000000000..d8ee070ee Binary files /dev/null and b/tests/e2e/detectors/test_data/oracle-spot-price/0.8.20/spot_price_balanceOf.sol-0.8.20.zip differ diff --git a/tests/e2e/detectors/test_data/oracle-spot-price/0.8.20/spot_price_getAmountOut.sol b/tests/e2e/detectors/test_data/oracle-spot-price/0.8.20/spot_price_getAmountOut.sol new file mode 100644 index 000000000..3571ee0be --- /dev/null +++ b/tests/e2e/detectors/test_data/oracle-spot-price/0.8.20/spot_price_getAmountOut.sol @@ -0,0 +1,73 @@ +pragma solidity 0.8.20; + +interface IERC20 { + function balanceOf(address account) external view returns (uint256); + + /** + * @dev Moves a `value` amount of tokens from the caller's account to `to`. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transfer(address to, uint256 value) external returns (bool); + + function decimals() external view virtual returns (uint8); +} +interface IUniswapV2Pair { + function getReserves() + external + view + returns (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast); +} +interface IUniswapV2Factory { + function getPair( + address tokenA, + address tokenB + ) external view returns (address pair); +} + +interface IUniswapV2Router01 { + function quote( + uint amountA, + uint reserveA, + uint reserveB + ) external pure returns (uint amountB); + function getAmountOut( + uint amountIn, + uint reserveIn, + uint reserveOut + ) external pure returns (uint amountOut); +} + +contract getAmountOut { + // Same address just for testing purposes + address UNISWAP_ROUTER = + address(0x96871914D0F4354A79B1E4651b464351e093b737); + address UNISWAP_FACTORY = + address(0x96871914D0F4354A79B1E4651b464351e093b737); + address USDC = address(0x96871914D0F4354A79B1E4651b464351e093b737); + address WETH = address(0x96871914D0F4354A79B1E4651b464351e093b737); + + function getEthUsdPrice() public view returns (uint256) { + address pairAddress = IUniswapV2Factory(UNISWAP_FACTORY).getPair( + USDC, + WETH + ); + require(pairAddress != address(0x00), "pair not found"); + IUniswapV2Pair pair = IUniswapV2Pair(pairAddress); + (uint256 left, uint256 right, ) = pair.getReserves(); + (uint256 usdcReserves, uint256 ethReserves) = (USDC < WETH) + ? (left, right) + : (right, left); + uint8 ethDecimals = IERC20(WETH).decimals(); + //uint8 usdcDecimals = ERC20(USDC).decimals(); + //returns price in 6 decimals + return + IUniswapV2Router01(UNISWAP_ROUTER).getAmountOut( + 10 ** ethDecimals, + ethReserves, + usdcReserves + ); + } +} diff --git a/tests/e2e/detectors/test_data/oracle-spot-price/0.8.20/spot_price_getAmountOut.sol-0.8.20.zip b/tests/e2e/detectors/test_data/oracle-spot-price/0.8.20/spot_price_getAmountOut.sol-0.8.20.zip new file mode 100644 index 000000000..f14e0c61d Binary files /dev/null and b/tests/e2e/detectors/test_data/oracle-spot-price/0.8.20/spot_price_getAmountOut.sol-0.8.20.zip differ diff --git a/tests/e2e/detectors/test_detectors.py b/tests/e2e/detectors/test_detectors.py index 5604b57dd..8c9aa40f3 100644 --- a/tests/e2e/detectors/test_detectors.py +++ b/tests/e2e/detectors/test_detectors.py @@ -1869,6 +1869,9 @@ def id_test(test_item: Test): "C.sol", "0.8.16", ), + Test(all_detectors.SpotPriceDetector, "spot_price_getReserves.sol", "0.6.12"), + Test(all_detectors.SpotPriceDetector, "spot_price_balanceOf.sol", "0.8.20"), + Test(all_detectors.SpotPriceDetector, "spot_price_getAmountOut.sol", "0.8.20"), ] GENERIC_PATH = "/GENERIC_PATH"