From 1fd39891f1f217f34dddb637e5159823795717a5 Mon Sep 17 00:00:00 2001 From: Alexis Date: Mon, 10 Jun 2024 17:27:42 +0200 Subject: [PATCH 1/4] Fix #2477 --- slither/core/scope/scope.py | 14 +- slither/core/slither_core.py | 2 +- slither/detectors/statements/unused_import.py | 4 +- slither/slither.py | 7 +- .../slither_compilation_unit_solc.py | 10 +- ...ort_0_8_16_CrossDomainMessenger_sol__0.txt | 3 + .../unused-import/0.8.16/Constants.sol | 53 ++++ .../0.8.16/CrossDomainMessenger.sol | 260 ++++++++++++++++++ .../CrossDomainMessenger.sol-0.8.16.zip | Bin 0 -> 31305 bytes .../0.8.16/lib/ResourceMetering.sol | 153 +++++++++++ .../unused-import/0.8.16/utils/console.sol | 8 + .../0.8.16/utils/original/Initializable.sol | 178 ++++++++++++ .../utils/upgradeable/Initializable.sol | 228 +++++++++++++++ tests/e2e/detectors/test_detectors.py | 7 + 14 files changed, 918 insertions(+), 9 deletions(-) create mode 100644 tests/e2e/detectors/snapshots/detectors__detector_UnusedImport_0_8_16_CrossDomainMessenger_sol__0.txt create mode 100644 tests/e2e/detectors/test_data/unused-import/0.8.16/Constants.sol create mode 100644 tests/e2e/detectors/test_data/unused-import/0.8.16/CrossDomainMessenger.sol create mode 100644 tests/e2e/detectors/test_data/unused-import/0.8.16/CrossDomainMessenger.sol-0.8.16.zip create mode 100644 tests/e2e/detectors/test_data/unused-import/0.8.16/lib/ResourceMetering.sol create mode 100644 tests/e2e/detectors/test_data/unused-import/0.8.16/utils/console.sol create mode 100644 tests/e2e/detectors/test_data/unused-import/0.8.16/utils/original/Initializable.sol create mode 100644 tests/e2e/detectors/test_data/unused-import/0.8.16/utils/upgradeable/Initializable.sol diff --git a/slither/core/scope/scope.py b/slither/core/scope/scope.py index ee2a98eb3a..5e0241309b 100644 --- a/slither/core/scope/scope.py +++ b/slither/core/scope/scope.py @@ -29,7 +29,7 @@ class FileScope: def __init__(self, filename: Filename) -> None: self.filename = filename self.accessible_scopes: List[FileScope] = [] - self.exported_symbols: Set[int] = set() + self.exported_symbols: List[int] = [] self.contracts: Dict[str, Contract] = {} # Custom error are a list instead of a dict @@ -82,8 +82,16 @@ def add_accessible_scopes(self) -> bool: # pylint: disable=too-many-branches # To get around this bug for aliases https://github.com/ethereum/solidity/pull/11881, # we propagate the exported_symbols from the imported file to the importing file # See tests/e2e/solc_parsing/test_data/top-level-nested-import-0.7.1.sol - if not new_scope.exported_symbols.issubset(self.exported_symbols): - self.exported_symbols |= new_scope.exported_symbols + if not set(new_scope.exported_symbols).issubset(self.exported_symbols): + # We are using lists and specifically extending them to keep the order in which + # elements are added. This will come handy when we have name collisions. + # See issue : + new_symbols = [ + symbol + for symbol in new_scope.exported_symbols + if symbol not in self.exported_symbols + ] + self.exported_symbols.extend(new_symbols) learn_something = True # This is need to support aliasing when we do a late lookup using SolidityImportPlaceholder diff --git a/slither/core/slither_core.py b/slither/core/slither_core.py index 1206e564bc..aacd43e66c 100644 --- a/slither/core/slither_core.py +++ b/slither/core/slither_core.py @@ -251,7 +251,7 @@ def _compute_offsets_from_thing(self, thing: SourceMapping): self._offset_to_definitions[ref.filename][offset].add(definition) self._offset_to_implementations[ref.filename][offset].update(implementations) - self._offset_to_references[ref.filename][offset] |= set(references) + self._offset_to_references[ref.filename][offset].add(thing.source_mapping) def _compute_offsets_to_ref_impl_decl(self): # pylint: disable=too-many-branches self._offset_to_references = defaultdict(lambda: defaultdict(lambda: set())) diff --git a/slither/detectors/statements/unused_import.py b/slither/detectors/statements/unused_import.py index d3447dcd81..e0ab236616 100644 --- a/slither/detectors/statements/unused_import.py +++ b/slither/detectors/statements/unused_import.py @@ -92,9 +92,9 @@ def _detect(self) -> List[Output]: # pylint: disable=too-many-branches use_found = False # Search through all references to the imported file - for _, refs_to_imported_path in self.slither._offset_to_references[ + for refs_to_imported_path in self.slither._offset_to_references[ imported_path - ].items(): + ].values(): for ref in refs_to_imported_path: # If there is a reference in this file to the imported file, it is used. if ref.filename == filename: diff --git a/slither/slither.py b/slither/slither.py index 7adc0694ca..b565cc12df 100644 --- a/slither/slither.py +++ b/slither/slither.py @@ -56,7 +56,12 @@ def _update_file_scopes( for refId in scope.exported_symbols: if refId in sol_parser.contracts_by_id: contract = sol_parser.contracts_by_id[refId] - scope.contracts[contract.name] = contract + + # Only add elements if they are not present. Since we kept the exported symbols in + # we resolve from the most local imports first. + if contract.name not in scope.contracts: + scope.contracts[contract.name] = contract + elif refId in sol_parser.functions_by_id: functions = sol_parser.functions_by_id[refId] assert len(functions) == 1 diff --git a/slither/solc_parsing/slither_compilation_unit_solc.py b/slither/solc_parsing/slither_compilation_unit_solc.py index 36efeef33a..848e2965b8 100644 --- a/slither/solc_parsing/slither_compilation_unit_solc.py +++ b/slither/solc_parsing/slither_compilation_unit_solc.py @@ -3,6 +3,7 @@ import logging import os import re +from itertools import chain from pathlib import Path from typing import List, Dict @@ -256,8 +257,13 @@ def parse_top_level_items(self, data_loaded: Dict, filename: str) -> None: scope = self.compilation_unit.get_scope(filename) # Exported symbols includes a reference ID to all top-level definitions the file exports, # including def's brought in by imports (even transitively) and def's local to the file. - for refId in exported_symbols.values(): - scope.exported_symbols |= set(refId) + + new_symbols = [ + symbol + for symbol in chain.from_iterable(exported_symbols.values()) + if symbol not in scope.exported_symbols + ] + scope.exported_symbols.extend(new_symbols) for top_level_data in data_loaded[self.get_children()]: if top_level_data[self.get_key()] == "ContractDefinition": diff --git a/tests/e2e/detectors/snapshots/detectors__detector_UnusedImport_0_8_16_CrossDomainMessenger_sol__0.txt b/tests/e2e/detectors/snapshots/detectors__detector_UnusedImport_0_8_16_CrossDomainMessenger_sol__0.txt new file mode 100644 index 0000000000..15d6b29ef1 --- /dev/null +++ b/tests/e2e/detectors/snapshots/detectors__detector_UnusedImport_0_8_16_CrossDomainMessenger_sol__0.txt @@ -0,0 +1,3 @@ +The following unused import(s) in tests/e2e/detectors/test_data/unused-import/0.8.16/CrossDomainMessenger.sol should be removed: + -import "./utils/console.sol"; (tests/e2e/detectors/test_data/unused-import/0.8.16/CrossDomainMessenger.sol#6) + diff --git a/tests/e2e/detectors/test_data/unused-import/0.8.16/Constants.sol b/tests/e2e/detectors/test_data/unused-import/0.8.16/Constants.sol new file mode 100644 index 0000000000..106151daee --- /dev/null +++ b/tests/e2e/detectors/test_data/unused-import/0.8.16/Constants.sol @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.16; + +import { ResourceMetering } from "./lib/ResourceMetering.sol"; + +/// @title Constants +/// @notice Constants is a library for storing constants. Simple! Don't put everything in here, just +/// the stuff used in multiple contracts. Constants that only apply to a single contract +/// should be defined in that contract instead. +library Constants { + /// @notice Special address to be used as the tx origin for gas estimation calls in the + /// OptimismPortal and CrossDomainMessenger calls. You only need to use this address if + /// the minimum gas limit specified by the user is not actually enough to execute the + /// given message and you're attempting to estimate the actual necessary gas limit. We + /// use address(1) because it's the ecrecover precompile and therefore guaranteed to + /// never have any code on any EVM chain. + address internal constant ESTIMATION_ADDRESS = address(1); + + /// @notice Value used for the L2 sender storage slot in both the OptimismPortal and the + /// CrossDomainMessenger contracts before an actual sender is set. This value is + /// non-zero to reduce the gas cost of message passing transactions. + address internal constant DEFAULT_L2_SENDER = 0x000000000000000000000000000000000000dEaD; + + /// @notice The storage slot that holds the address of a proxy implementation. + /// @dev `bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1)` + bytes32 internal constant PROXY_IMPLEMENTATION_ADDRESS = + 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; + + /// @notice The storage slot that holds the address of the owner. + /// @dev `bytes32(uint256(keccak256('eip1967.proxy.admin')) - 1)` + bytes32 internal constant PROXY_OWNER_ADDRESS = 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103; + + /// @notice The address that represents ether when dealing with ERC20 token addresses. + address internal constant ETHER = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + + /// @notice The address that represents the system caller responsible for L1 attributes + /// transactions. + address internal constant DEPOSITOR_ACCOUNT = 0xDeaDDEaDDeAdDeAdDEAdDEaddeAddEAdDEAd0001; + + /// @notice Returns the default values for the ResourceConfig. These are the recommended values + /// for a production network. + function DEFAULT_RESOURCE_CONFIG() internal pure returns (ResourceMetering.ResourceConfig memory) { + ResourceMetering.ResourceConfig memory config = ResourceMetering.ResourceConfig({ + maxResourceLimit: 20_000_000, + elasticityMultiplier: 10, + baseFeeMaxChangeDenominator: 8, + minimumBaseFee: 1 gwei, + systemTxMaxGas: 1_000_000, + maximumBaseFee: type(uint128).max + }); + return config; + } +} diff --git a/tests/e2e/detectors/test_data/unused-import/0.8.16/CrossDomainMessenger.sol b/tests/e2e/detectors/test_data/unused-import/0.8.16/CrossDomainMessenger.sol new file mode 100644 index 0000000000..425421140d --- /dev/null +++ b/tests/e2e/detectors/test_data/unused-import/0.8.16/CrossDomainMessenger.sol @@ -0,0 +1,260 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.16; + +import { Initializable } from "./utils/upgradeable/Initializable.sol"; +import { Constants } from "./Constants.sol"; +import "./utils/console.sol"; + + +/// @custom:upgradeable +/// @title CrossDomainMessenger +/// @notice CrossDomainMessenger is a base contract that provides the core logic for the L1 and L2 +/// cross-chain messenger contracts. It's designed to be a universal interface that only +/// needs to be extended slightly to provide low-level message passing functionality on each +/// chain it's deployed on. Currently only designed for message passing between two paired +/// chains and does not support one-to-many interactions. +/// Any changes to this contract MUST result in a semver bump for contracts that inherit it. +abstract contract CrossDomainMessenger is + Initializable +{ + /// @notice Current message version identifier. + uint16 public constant MESSAGE_VERSION = 1; + + /// @notice Constant overhead added to the base gas for a message. + uint64 public constant RELAY_CONSTANT_OVERHEAD = 200_000; + + /// @notice Numerator for dynamic overhead added to the base gas for a message. + uint64 public constant MIN_GAS_DYNAMIC_OVERHEAD_NUMERATOR = 64; + + /// @notice Denominator for dynamic overhead added to the base gas for a message. + uint64 public constant MIN_GAS_DYNAMIC_OVERHEAD_DENOMINATOR = 63; + + /// @notice Extra gas added to base gas for each byte of calldata in a message. + uint64 public constant MIN_GAS_CALLDATA_OVERHEAD = 16; + + /// @notice Gas reserved for performing the external call in `relayMessage`. + uint64 public constant RELAY_CALL_OVERHEAD = 40_000; + + /// @notice Gas reserved for finalizing the execution of `relayMessage` after the safe call. + uint64 public constant RELAY_RESERVED_GAS = 40_000; + + /// @notice Gas reserved for the execution between the `hasMinGas` check and the external + /// call in `relayMessage`. + uint64 public constant RELAY_GAS_CHECK_BUFFER = 5_000; + + /// @notice Mapping of message hashes to boolean receipt values. Note that a message will only + /// be present in this mapping if it has successfully been relayed on this chain, and + /// can therefore not be relayed again. + mapping(bytes32 => bool) public successfulMessages; + + /// @notice Address of the sender of the currently executing message on the other chain. If the + /// value of this variable is the default value (0x00000000...dead) then no message is + /// currently being executed. Use the xDomainMessageSender getter which will throw an + /// error if this is the case. + address internal xDomainMsgSender; + + /// @notice Nonce for the next message to be sent, without the message version applied. Use the + /// messageNonce getter which will insert the message version into the nonce to give you + /// the actual nonce to be used for the message. + uint240 internal msgNonce; + + /// @notice Mapping of message hashes to a boolean if and only if the message has failed to be + /// executed at least once. A message will not be present in this mapping if it + /// successfully executed on the first attempt. + mapping(bytes32 => bool) public failedMessages; + + /// @notice CrossDomainMessenger contract on the other chain. + /// @custom:network-specific + CrossDomainMessenger public otherMessenger; + + /// @notice Reserve extra slots in the storage layout for future upgrades. + /// A gap size of 43 was chosen here, so that the first slot used in a child contract + /// would be 1 plus a multiple of 50. + uint256[43] private __gap; + + /// @notice Emitted whenever a message is sent to the other chain. + /// @param target Address of the recipient of the message. + /// @param sender Address of the sender of the message. + /// @param message Message to trigger the recipient address with. + /// @param messageNonce Unique nonce attached to the message. + /// @param gasLimit Minimum gas limit that the message can be executed with. + event SentMessage(address indexed target, address sender, bytes message, uint256 messageNonce, uint256 gasLimit); + + /// @notice Additional event data to emit, required as of Bedrock. Cannot be merged with the + /// SentMessage event without breaking the ABI of this contract, this is good enough. + /// @param sender Address of the sender of the message. + /// @param value ETH value sent along with the message to the recipient. + event SentMessageExtension1(address indexed sender, uint256 value); + + /// @notice Emitted whenever a message is successfully relayed on this chain. + /// @param msgHash Hash of the message that was relayed. + event RelayedMessage(bytes32 indexed msgHash); + + /// @notice Emitted whenever a message fails to be relayed on this chain. + /// @param msgHash Hash of the message that failed to be relayed. + event FailedRelayedMessage(bytes32 indexed msgHash); + + /// @notice Sends a message to some target address on the other chain. Note that if the call + /// always reverts, then the message will be unrelayable, and any ETH sent will be + /// permanently locked. The same will occur if the target on the other chain is + /// considered unsafe (see the _isUnsafeTarget() function). + /// @param _target Target contract or wallet address. + /// @param _message Message to trigger the target address with. + /// @param _minGasLimit Minimum gas limit that the message can be executed with. + function sendMessage(address _target, bytes calldata _message, uint32 _minGasLimit) external payable { + if (isCustomGasToken()) { + require(msg.value == 0, "CrossDomainMessenger: cannot send value with custom gas token"); + } + + // Triggers a message to the other messenger. Note that the amount of gas provided to the + // message is the amount of gas requested by the user PLUS the base gas value. We want to + // guarantee the property that the call to the target contract will always have at least + // the minimum gas limit specified by the user. + _sendMessage({ + _to: address(otherMessenger), + _gasLimit: baseGas(_message, _minGasLimit), + _value: msg.value, + _data: abi.encodeWithSelector( + this.relayMessage.selector, messageNonce(), msg.sender, _target, msg.value, _minGasLimit, _message + ) + }); + + emit SentMessage(_target, msg.sender, _message, messageNonce(), _minGasLimit); + emit SentMessageExtension1(msg.sender, msg.value); + + unchecked { + ++msgNonce; + } + } + + /// @notice Relays a message that was sent by the other CrossDomainMessenger contract. Can only + /// be executed via cross-chain call from the other messenger OR if the message was + /// already received once and is currently being replayed. + /// @param _nonce Nonce of the message being relayed. + /// @param _sender Address of the user who sent the message. + /// @param _target Address that the message is targeted at. + /// @param _value ETH value to send with the message. + /// @param _minGasLimit Minimum amount of gas that the message can be executed with. + /// @param _message Message to send to the target. + function relayMessage( + uint256 _nonce, + address _sender, + address _target, + uint256 _value, + uint256 _minGasLimit, + bytes calldata _message + ) + external + payable + { + return; + } + + /// @notice Retrieves the address of the contract or wallet that initiated the currently + /// executing message on the other chain. Will throw an error if there is no message + /// currently being executed. Allows the recipient of a call to see who triggered it. + /// @return Address of the sender of the currently executing message on the other chain. + function xDomainMessageSender() external view returns (address) { + require( + xDomainMsgSender != Constants.DEFAULT_L2_SENDER, "CrossDomainMessenger: xDomainMessageSender is not set" + ); + + return xDomainMsgSender; + } + + /// @notice Retrieves the address of the paired CrossDomainMessenger contract on the other chain + /// Public getter is legacy and will be removed in the future. Use `otherMessenger()` instead. + /// @return CrossDomainMessenger contract on the other chain. + /// @custom:legacy + function OTHER_MESSENGER() public view returns (CrossDomainMessenger) { + return otherMessenger; + } + + /// @notice Retrieves the next message nonce. Message version will be added to the upper two + /// bytes of the message nonce. Message version allows us to treat messages as having + /// different structures. + /// @return Nonce of the next message to be sent, with added message version. + function messageNonce() public view returns (uint256) { + return 1; + } + + /// @notice Computes the amount of gas required to guarantee that a given message will be + /// received on the other chain without running out of gas. Guaranteeing that a message + /// will not run out of gas is important because this ensures that a message can always + /// be replayed on the other chain if it fails to execute completely. + /// @param _message Message to compute the amount of required gas for. + /// @param _minGasLimit Minimum desired gas limit when message goes to target. + /// @return Amount of gas required to guarantee message receipt. + function baseGas(bytes calldata _message, uint32 _minGasLimit) public pure returns (uint64) { + return + // Constant overhead + RELAY_CONSTANT_OVERHEAD + // Calldata overhead + + (uint64(_message.length) * MIN_GAS_CALLDATA_OVERHEAD) + // Dynamic overhead (EIP-150) + + ((_minGasLimit * MIN_GAS_DYNAMIC_OVERHEAD_NUMERATOR) / MIN_GAS_DYNAMIC_OVERHEAD_DENOMINATOR) + // Gas reserved for the worst-case cost of 3/5 of the `CALL` opcode's dynamic gas + // factors. (Conservative) + + RELAY_CALL_OVERHEAD + // Relay reserved gas (to ensure execution of `relayMessage` completes after the + // subcontext finishes executing) (Conservative) + + RELAY_RESERVED_GAS + // Gas reserved for the execution between the `hasMinGas` check and the `CALL` + // opcode. (Conservative) + + RELAY_GAS_CHECK_BUFFER; + } + + /// @notice Returns the address of the gas token and the token's decimals. + function gasPayingToken() internal view virtual returns (address, uint8); + + /// @notice Returns whether the chain uses a custom gas token or not. + function isCustomGasToken() internal view returns (bool) { + (address token,) = gasPayingToken(); + return token != Constants.ETHER; + } + + /// @notice Initializer. + /// @param _otherMessenger CrossDomainMessenger contract on the other chain. + function __CrossDomainMessenger_init(CrossDomainMessenger _otherMessenger) internal onlyInitializing { + // We only want to set the xDomainMsgSender to the default value if it hasn't been initialized yet, + // meaning that this is a fresh contract deployment. + // This prevents resetting the xDomainMsgSender to the default value during an upgrade, which would enable + // a reentrant withdrawal to sandwhich the upgrade replay a withdrawal twice. + if (xDomainMsgSender == address(0)) { + xDomainMsgSender = Constants.DEFAULT_L2_SENDER; + } + otherMessenger = _otherMessenger; + } + + /// @notice Sends a low-level message to the other messenger. Needs to be implemented by child + /// contracts because the logic for this depends on the network where the messenger is + /// being deployed. + /// @param _to Recipient of the message on the other chain. + /// @param _gasLimit Minimum gas limit the message can be executed with. + /// @param _value Amount of ETH to send with the message. + /// @param _data Message data. + function _sendMessage(address _to, uint64 _gasLimit, uint256 _value, bytes memory _data) internal virtual; + + /// @notice Checks whether the message is coming from the other messenger. Implemented by child + /// contracts because the logic for this depends on the network where the messenger is + /// being deployed. + /// @return Whether the message is coming from the other messenger. + function _isOtherMessenger() internal view virtual returns (bool); + + /// @notice Checks whether a given call target is a system address that could cause the + /// messenger to peform an unsafe action. This is NOT a mechanism for blocking user + /// addresses. This is ONLY used to prevent the execution of messages to specific + /// system addresses that could cause security issues, e.g., having the + /// CrossDomainMessenger send messages to itself. + /// @param _target Address of the contract to check. + /// @return Whether or not the address is an unsafe system address. + function _isUnsafeTarget(address _target) internal view virtual returns (bool); + + /// @notice This function should return true if the contract is paused. + /// On L1 this function will check the SuperchainConfig for its paused status. + /// On L2 this function should be a no-op. + /// @return Whether or not the contract is paused. + function paused() public view virtual returns (bool) { + return false; + } +} diff --git a/tests/e2e/detectors/test_data/unused-import/0.8.16/CrossDomainMessenger.sol-0.8.16.zip b/tests/e2e/detectors/test_data/unused-import/0.8.16/CrossDomainMessenger.sol-0.8.16.zip new file mode 100644 index 0000000000000000000000000000000000000000..6cb35358a727ec1447429a2f3f5a497e091cae46 GIT binary patch literal 31305 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 literal 0 HcmV?d00001 diff --git a/tests/e2e/detectors/test_data/unused-import/0.8.16/lib/ResourceMetering.sol b/tests/e2e/detectors/test_data/unused-import/0.8.16/lib/ResourceMetering.sol new file mode 100644 index 0000000000..024d22e8bb --- /dev/null +++ b/tests/e2e/detectors/test_data/unused-import/0.8.16/lib/ResourceMetering.sol @@ -0,0 +1,153 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.16; + +import { Initializable } from "../utils/original/Initializable.sol"; + + +/// @custom:upgradeable +/// @title ResourceMetering +/// @notice ResourceMetering implements an EIP-1559 style resource metering system where pricing +/// updates automatically based on current demand. +abstract contract ResourceMetering is Initializable { + /// @notice Error returned when too much gas resource is consumed. + error OutOfGas(); + + /// @notice Represents the various parameters that control the way in which resources are + /// metered. Corresponds to the EIP-1559 resource metering system. + /// @custom:field prevBaseFee Base fee from the previous block(s). + /// @custom:field prevBoughtGas Amount of gas bought so far in the current block. + /// @custom:field prevBlockNum Last block number that the base fee was updated. + struct ResourceParams { + uint128 prevBaseFee; + uint64 prevBoughtGas; + uint64 prevBlockNum; + } + + /// @notice Represents the configuration for the EIP-1559 based curve for the deposit gas + /// market. These values should be set with care as it is possible to set them in + /// a way that breaks the deposit gas market. The target resource limit is defined as + /// maxResourceLimit / elasticityMultiplier. This struct was designed to fit within a + /// single word. There is additional space for additions in the future. + /// @custom:field maxResourceLimit Represents the maximum amount of deposit gas that + /// can be purchased per block. + /// @custom:field elasticityMultiplier Determines the target resource limit along with + /// the resource limit. + /// @custom:field baseFeeMaxChangeDenominator Determines max change on fee per block. + /// @custom:field minimumBaseFee The min deposit base fee, it is clamped to this + /// value. + /// @custom:field systemTxMaxGas The amount of gas supplied to the system + /// transaction. This should be set to the same + /// number that the op-node sets as the gas limit + /// for the system transaction. + /// @custom:field maximumBaseFee The max deposit base fee, it is clamped to this + /// value. + struct ResourceConfig { + uint32 maxResourceLimit; + uint8 elasticityMultiplier; + uint8 baseFeeMaxChangeDenominator; + uint32 minimumBaseFee; + uint32 systemTxMaxGas; + uint128 maximumBaseFee; + } + + /// @notice EIP-1559 style gas parameters. + ResourceParams public params; + + /// @notice Reserve extra slots (to a total of 50) in the storage layout for future upgrades. + uint256[48] private __gap; + + /// @notice Meters access to a function based an amount of a requested resource. + /// @param _amount Amount of the resource requested. + modifier metered(uint64 _amount) { + // Record initial gas amount so we can refund for it later. + uint256 initialGas = gasleft(); + + // Run the underlying function. + _; + + // Run the metering function. + _metered(_amount, initialGas); + } + + /// @notice An internal function that holds all of the logic for metering a resource. + /// @param _amount Amount of the resource requested. + /// @param _initialGas The amount of gas before any modifier execution. + function _metered(uint64 _amount, uint256 _initialGas) internal { + // Update block number and base fee if necessary. + uint256 blockDiff = block.number - params.prevBlockNum; + + ResourceConfig memory config = _resourceConfig(); + int256 targetResourceLimit = + int256(uint256(config.maxResourceLimit)) / int256(uint256(config.elasticityMultiplier)); + + if (blockDiff > 0) { + // Handle updating EIP-1559 style gas parameters. We use EIP-1559 to restrict the rate + // at which deposits can be created and therefore limit the potential for deposits to + // spam the L2 system. Fee scheme is very similar to EIP-1559 with minor changes. + int256 gasUsedDelta = int256(uint256(params.prevBoughtGas)) - targetResourceLimit; + int256 baseFeeDelta = (int256(uint256(params.prevBaseFee)) * gasUsedDelta) + / (targetResourceLimit * int256(uint256(config.baseFeeMaxChangeDenominator))); + + // Update base fee by adding the base fee delta and clamp the resulting value between + // min and max. + int256 newBaseFee = 0; + + // If we skipped more than one block, we also need to account for every empty block. + // Empty block means there was no demand for deposits in that block, so we should + // reflect this lack of demand in the fee. + if (blockDiff > 1) { + // Update the base fee by repeatedly applying the exponent 1-(1/change_denominator) + // blockDiff - 1 times. Simulates multiple empty blocks. Clamp the resulting value + // between min and max. + newBaseFee = 0; + } + + // Update new base fee, reset bought gas, and update block number. + params.prevBaseFee = uint128(uint256(newBaseFee)); + params.prevBoughtGas = 0; + params.prevBlockNum = uint64(block.number); + } + + // Make sure we can actually buy the resource amount requested by the user. + params.prevBoughtGas += _amount; + if (int256(uint256(params.prevBoughtGas)) > int256(uint256(config.maxResourceLimit))) { + revert OutOfGas(); + } + + // Determine the amount of ETH to be paid. + uint256 resourceCost = uint256(_amount) * uint256(params.prevBaseFee); + + // We currently charge for this ETH amount as an L1 gas burn, so we convert the ETH amount + // into gas by dividing by the L1 base fee. We assume a minimum base fee of 1 gwei to avoid + // division by zero for L1s that don't support 1559 or to avoid excessive gas burns during + // periods of extremely low L1 demand. One-day average gas fee hasn't dipped below 1 gwei + // during any 1 day period in the last 5 years, so should be fine. + uint256 gasCost = resourceCost / 1; + + // Give the user a refund based on the amount of gas they used to do all of the work up to + // this point. Since we're at the end of the modifier, this should be pretty accurate. Acts + // effectively like a dynamic stipend (with a minimum value). + uint256 usedGas = _initialGas - gasleft(); + } + + /// @notice Adds an amount of L2 gas consumed to the prev bought gas params. This is meant to be used + /// when L2 system transactions are generated from L1. + /// @param _amount Amount of the L2 gas resource requested. + function useGas(uint32 _amount) internal { + params.prevBoughtGas += uint64(_amount); + } + + /// @notice Virtual function that returns the resource config. + /// Contracts that inherit this contract must implement this function. + /// @return ResourceConfig + function _resourceConfig() internal virtual returns (ResourceConfig memory); + + /// @notice Sets initial resource parameter values. + /// This function must either be called by the initializer function of an upgradeable + /// child contract. + function __ResourceMetering_init() internal onlyInitializing { + if (params.prevBlockNum == 0) { + params = ResourceParams({ prevBaseFee: 1 gwei, prevBoughtGas: 0, prevBlockNum: uint64(block.number) }); + } + } +} diff --git a/tests/e2e/detectors/test_data/unused-import/0.8.16/utils/console.sol b/tests/e2e/detectors/test_data/unused-import/0.8.16/utils/console.sol new file mode 100644 index 0000000000..966aaafd77 --- /dev/null +++ b/tests/e2e/detectors/test_data/unused-import/0.8.16/utils/console.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.16; + +contract Console { + constructor(){ + + } +} diff --git a/tests/e2e/detectors/test_data/unused-import/0.8.16/utils/original/Initializable.sol b/tests/e2e/detectors/test_data/unused-import/0.8.16/utils/original/Initializable.sol new file mode 100644 index 0000000000..a613b63164 --- /dev/null +++ b/tests/e2e/detectors/test_data/unused-import/0.8.16/utils/original/Initializable.sol @@ -0,0 +1,178 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (proxy/utils/Initializable.sol) + +pragma solidity ^0.8.16; + +abstract contract Initializable { + /** + * @dev Storage of the initializable contract. + * + * It's implemented on a custom ERC-7201 namespace to reduce the risk of storage collisions + * when using with upgradeable contracts. + * + * @custom:storage-location erc7201:openzeppelin.storage.Initializable + */ + struct InitializableStorage { + /** + * @dev Indicates that the contract has been initialized. + */ + uint64 _initialized; + /** + * @dev Indicates that the contract is in the process of being initialized. + */ + bool _initializing; + } + + // keccak256(abi.encode(uint256(keccak256("openzeppelin.storage.Initializable")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 private constant INITIALIZABLE_STORAGE = 0xf0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a00; + + /** + * @dev The contract is already initialized. + */ + error InvalidInitialization(); + + /** + * @dev The contract is not initializing. + */ + error NotInitializing(); + + /** + * @dev Triggered when the contract has been initialized or reinitialized. + */ + event Initialized(uint64 version); + + /** + * @dev A modifier that defines a protected initializer function that can be invoked at most once. In its scope, + * `onlyInitializing` functions can be used to initialize parent contracts. + * + * Similar to `reinitializer(1)`, except that in the context of a constructor an `initializer` may be invoked any + * number of times. This behavior in the constructor can be useful during testing and is not expected to be used in + * production. + * + * Emits an {Initialized} event. + */ + modifier initializer() { + // solhint-disable-next-line var-name-mixedcase + InitializableStorage storage $ = _getInitializableStorage(); + + // Cache values to avoid duplicated sloads + bool isTopLevelCall = !$._initializing; + uint64 initialized = $._initialized; + + // Allowed calls: + // - initialSetup: the contract is not in the initializing state and no previous version was + // initialized + // - construction: the contract is initialized at version 1 (no reininitialization) and the + // current contract is just being deployed + bool initialSetup = initialized == 0 && isTopLevelCall; + bool construction = initialized == 1 && address(this).code.length == 0; + + if (!initialSetup && !construction) { + revert InvalidInitialization(); + } + $._initialized = 1; + if (isTopLevelCall) { + $._initializing = true; + } + _; + if (isTopLevelCall) { + $._initializing = false; + emit Initialized(1); + } + } + + /** + * @dev A modifier that defines a protected reinitializer function that can be invoked at most once, and only if the + * contract hasn't been initialized to a greater version before. In its scope, `onlyInitializing` functions can be + * used to initialize parent contracts. + * + * A reinitializer may be used after the original initialization step. This is essential to configure modules that + * are added through upgrades and that require initialization. + * + * When `version` is 1, this modifier is similar to `initializer`, except that functions marked with `reinitializer` + * cannot be nested. If one is invoked in the context of another, execution will revert. + * + * Note that versions can jump in increments greater than 1; this implies that if multiple reinitializers coexist in + * a contract, executing them in the right order is up to the developer or operator. + * + * WARNING: Setting the version to 2**64 - 1 will prevent any future reinitialization. + * + * Emits an {Initialized} event. + */ + modifier reinitializer(uint64 version) { + // solhint-disable-next-line var-name-mixedcase + InitializableStorage storage $ = _getInitializableStorage(); + + if ($._initializing || $._initialized >= version) { + revert InvalidInitialization(); + } + $._initialized = version; + $._initializing = true; + _; + $._initializing = false; + emit Initialized(version); + } + + /** + * @dev Modifier to protect an initialization function so that it can only be invoked by functions with the + * {initializer} and {reinitializer} modifiers, directly or indirectly. + */ + modifier onlyInitializing() { + _checkInitializing(); + _; + } + + /** + * @dev Reverts if the contract is not in an initializing state. See {onlyInitializing}. + */ + function _checkInitializing() internal view virtual { + if (!_isInitializing()) { + revert NotInitializing(); + } + } + + /** + * @dev Locks the contract, preventing any future reinitialization. This cannot be part of an initializer call. + * Calling this in the constructor of a contract will prevent that contract from being initialized or reinitialized + * to any version. It is recommended to use this to lock implementation contracts that are designed to be called + * through proxies. + * + * Emits an {Initialized} event the first time it is successfully executed. + */ + function _disableInitializers() internal virtual { + // solhint-disable-next-line var-name-mixedcase + InitializableStorage storage $ = _getInitializableStorage(); + + if ($._initializing) { + revert InvalidInitialization(); + } + if ($._initialized != type(uint64).max) { + $._initialized = type(uint64).max; + emit Initialized(type(uint64).max); + } + } + + /** + * @dev Returns the highest version that has been initialized. See {reinitializer}. + */ + function _getInitializedVersion() internal view returns (uint64) { + return _getInitializableStorage()._initialized; + } + + /** + * @dev Returns `true` if the contract is currently initializing. See {onlyInitializing}. + */ + function _isInitializing() internal view returns (bool) { + return _getInitializableStorage()._initializing; + } + + /** + * @dev Returns a pointer to the storage namespace. + */ + // solhint-disable-next-line var-name-mixedcase + function _getInitializableStorage() private pure returns (InitializableStorage storage $) { + assembly { + $.slot := INITIALIZABLE_STORAGE + } + } +} diff --git a/tests/e2e/detectors/test_data/unused-import/0.8.16/utils/upgradeable/Initializable.sol b/tests/e2e/detectors/test_data/unused-import/0.8.16/utils/upgradeable/Initializable.sol new file mode 100644 index 0000000000..404e2bc9ef --- /dev/null +++ b/tests/e2e/detectors/test_data/unused-import/0.8.16/utils/upgradeable/Initializable.sol @@ -0,0 +1,228 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (proxy/utils/Initializable.sol) + +pragma solidity ^0.8.16; + +/** + * @dev This is a base contract to aid in writing upgradeable contracts, or any kind of contract that will be deployed + * behind a proxy. Since proxied contracts do not make use of a constructor, it's common to move constructor logic to an + * external initializer function, usually called `initialize`. It then becomes necessary to protect this initializer + * function so it can only be called once. The {initializer} modifier provided by this contract will have this effect. + * + * The initialization functions use a version number. Once a version number is used, it is consumed and cannot be + * reused. This mechanism prevents re-execution of each "step" but allows the creation of new initialization steps in + * case an upgrade adds a module that needs to be initialized. + * + * For example: + * + * [.hljs-theme-light.nopadding] + * ```solidity + * contract MyToken is ERC20Upgradeable { + * function initialize() initializer public { + * __ERC20_init("MyToken", "MTK"); + * } + * } + * + * contract MyTokenV2 is MyToken, ERC20PermitUpgradeable { + * function initializeV2() reinitializer(2) public { + * __ERC20Permit_init("MyToken"); + * } + * } + * ``` + * + * TIP: To avoid leaving the proxy in an uninitialized state, the initializer function should be called as early as + * possible by providing the encoded function call as the `_data` argument to {ERC1967Proxy-constructor}. + * + * CAUTION: When used with inheritance, manual care must be taken to not invoke a parent initializer twice, or to ensure + * that all initializers are idempotent. This is not verified automatically as constructors are by Solidity. + * + * [CAUTION] + * ==== + * Avoid leaving a contract uninitialized. + * + * An uninitialized contract can be taken over by an attacker. This applies to both a proxy and its implementation + * contract, which may impact the proxy. To prevent the implementation contract from being used, you should invoke + * the {_disableInitializers} function in the constructor to automatically lock it when it is deployed: + * + * [.hljs-theme-light.nopadding] + * ``` + * /// @custom:oz-upgrades-unsafe-allow constructor + * constructor() { + * _disableInitializers(); + * } + * ``` + * ==== + */ +abstract contract Initializable { + /** + * @dev Storage of the initializable contract. + * + * It's implemented on a custom ERC-7201 namespace to reduce the risk of storage collisions + * when using with upgradeable contracts. + * + * @custom:storage-location erc7201:openzeppelin.storage.Initializable + */ + struct InitializableStorage { + /** + * @dev Indicates that the contract has been initialized. + */ + uint64 _initialized; + /** + * @dev Indicates that the contract is in the process of being initialized. + */ + bool _initializing; + } + + // keccak256(abi.encode(uint256(keccak256("openzeppelin.storage.Initializable")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 private constant INITIALIZABLE_STORAGE = 0xf0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a00; + + /** + * @dev The contract is already initialized. + */ + error InvalidInitialization(); + + /** + * @dev The contract is not initializing. + */ + error NotInitializing(); + + /** + * @dev Triggered when the contract has been initialized or reinitialized. + */ + event Initialized(uint64 version); + + /** + * @dev A modifier that defines a protected initializer function that can be invoked at most once. In its scope, + * `onlyInitializing` functions can be used to initialize parent contracts. + * + * Similar to `reinitializer(1)`, except that in the context of a constructor an `initializer` may be invoked any + * number of times. This behavior in the constructor can be useful during testing and is not expected to be used in + * production. + * + * Emits an {Initialized} event. + */ + modifier initializer() { + // solhint-disable-next-line var-name-mixedcase + InitializableStorage storage $ = _getInitializableStorage(); + + // Cache values to avoid duplicated sloads + bool isTopLevelCall = !$._initializing; + uint64 initialized = $._initialized; + + // Allowed calls: + // - initialSetup: the contract is not in the initializing state and no previous version was + // initialized + // - construction: the contract is initialized at version 1 (no reininitialization) and the + // current contract is just being deployed + bool initialSetup = initialized == 0 && isTopLevelCall; + bool construction = initialized == 1 && address(this).code.length == 0; + + if (!initialSetup && !construction) { + revert InvalidInitialization(); + } + $._initialized = 1; + if (isTopLevelCall) { + $._initializing = true; + } + _; + if (isTopLevelCall) { + $._initializing = false; + emit Initialized(1); + } + } + + /** + * @dev A modifier that defines a protected reinitializer function that can be invoked at most once, and only if the + * contract hasn't been initialized to a greater version before. In its scope, `onlyInitializing` functions can be + * used to initialize parent contracts. + * + * A reinitializer may be used after the original initialization step. This is essential to configure modules that + * are added through upgrades and that require initialization. + * + * When `version` is 1, this modifier is similar to `initializer`, except that functions marked with `reinitializer` + * cannot be nested. If one is invoked in the context of another, execution will revert. + * + * Note that versions can jump in increments greater than 1; this implies that if multiple reinitializers coexist in + * a contract, executing them in the right order is up to the developer or operator. + * + * WARNING: Setting the version to 2**64 - 1 will prevent any future reinitialization. + * + * Emits an {Initialized} event. + */ + modifier reinitializer(uint64 version) { + // solhint-disable-next-line var-name-mixedcase + InitializableStorage storage $ = _getInitializableStorage(); + + if ($._initializing || $._initialized >= version) { + revert InvalidInitialization(); + } + $._initialized = version; + $._initializing = true; + _; + $._initializing = false; + emit Initialized(version); + } + + /** + * @dev Modifier to protect an initialization function so that it can only be invoked by functions with the + * {initializer} and {reinitializer} modifiers, directly or indirectly. + */ + modifier onlyInitializing() { + _checkInitializing(); + _; + } + + /** + * @dev Reverts if the contract is not in an initializing state. See {onlyInitializing}. + */ + function _checkInitializing() internal view virtual { + if (!_isInitializing()) { + revert NotInitializing(); + } + } + + /** + * @dev Locks the contract, preventing any future reinitialization. This cannot be part of an initializer call. + * Calling this in the constructor of a contract will prevent that contract from being initialized or reinitialized + * to any version. It is recommended to use this to lock implementation contracts that are designed to be called + * through proxies. + * + * Emits an {Initialized} event the first time it is successfully executed. + */ + function _disableInitializers() internal virtual { + // solhint-disable-next-line var-name-mixedcase + InitializableStorage storage $ = _getInitializableStorage(); + + if ($._initializing) { + revert InvalidInitialization(); + } + if ($._initialized != type(uint64).max) { + $._initialized = type(uint64).max; + emit Initialized(type(uint64).max); + } + } + + /** + * @dev Returns the highest version that has been initialized. See {reinitializer}. + */ + function _getInitializedVersion() internal view returns (uint64) { + return _getInitializableStorage()._initialized; + } + + /** + * @dev Returns `true` if the contract is currently initializing. See {onlyInitializing}. + */ + function _isInitializing() internal view returns (bool) { + return _getInitializableStorage()._initializing; + } + + /** + * @dev Returns a pointer to the storage namespace. + */ + // solhint-disable-next-line var-name-mixedcase + function _getInitializableStorage() private pure returns (InitializableStorage storage $) { + assembly { + $.slot := INITIALIZABLE_STORAGE + } + } +} diff --git a/tests/e2e/detectors/test_detectors.py b/tests/e2e/detectors/test_detectors.py index 299f2ea031..83931c32b6 100644 --- a/tests/e2e/detectors/test_detectors.py +++ b/tests/e2e/detectors/test_detectors.py @@ -1,9 +1,11 @@ import os +import shutil from pathlib import Path import sys from typing import Type, Optional, List import pytest + from crytic_compile import CryticCompile, save_to_zip from crytic_compile.utils.zip import load_from_zip @@ -1899,6 +1901,11 @@ def id_test(test_item: Test): "C.sol", "0.8.16", ), + Test( + all_detectors.UnusedImport, + "CrossDomainMessenger.sol", + "0.8.16", + ), ] GENERIC_PATH = "/GENERIC_PATH" From 6eb48d40f5d84bd0aa64514d762d9a93c12ed1b6 Mon Sep 17 00:00:00 2001 From: Alexis Date: Mon, 10 Jun 2024 17:49:29 +0200 Subject: [PATCH 2/4] Revert buggy change. --- slither/core/slither_core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/slither/core/slither_core.py b/slither/core/slither_core.py index aacd43e66c..1206e564bc 100644 --- a/slither/core/slither_core.py +++ b/slither/core/slither_core.py @@ -251,7 +251,7 @@ def _compute_offsets_from_thing(self, thing: SourceMapping): self._offset_to_definitions[ref.filename][offset].add(definition) self._offset_to_implementations[ref.filename][offset].update(implementations) - self._offset_to_references[ref.filename][offset].add(thing.source_mapping) + self._offset_to_references[ref.filename][offset] |= set(references) def _compute_offsets_to_ref_impl_decl(self): # pylint: disable=too-many-branches self._offset_to_references = defaultdict(lambda: defaultdict(lambda: set())) From db5e0531c7ebccafd5102e0cb3c76d7cb2755738 Mon Sep 17 00:00:00 2001 From: Alexis Date: Mon, 10 Jun 2024 17:51:34 +0200 Subject: [PATCH 3/4] Improve comments --- slither/core/scope/scope.py | 2 +- slither/slither.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/slither/core/scope/scope.py b/slither/core/scope/scope.py index 5e0241309b..2fe40bdabb 100644 --- a/slither/core/scope/scope.py +++ b/slither/core/scope/scope.py @@ -85,7 +85,7 @@ def add_accessible_scopes(self) -> bool: # pylint: disable=too-many-branches if not set(new_scope.exported_symbols).issubset(self.exported_symbols): # We are using lists and specifically extending them to keep the order in which # elements are added. This will come handy when we have name collisions. - # See issue : + # See issue : https://github.com/crytic/slither/issues/2477 new_symbols = [ symbol for symbol in new_scope.exported_symbols diff --git a/slither/slither.py b/slither/slither.py index b565cc12df..4b30048dc1 100644 --- a/slither/slither.py +++ b/slither/slither.py @@ -57,8 +57,9 @@ def _update_file_scopes( if refId in sol_parser.contracts_by_id: contract = sol_parser.contracts_by_id[refId] - # Only add elements if they are not present. Since we kept the exported symbols in - # we resolve from the most local imports first. + # Add elements only if they are not already present. By keeping the exported symbols + # in the order they were encountered, we ensure that the most local imports are + # resolved first. if contract.name not in scope.contracts: scope.contracts[contract.name] = contract From 89af1d8fe9e33ad9ba94d2b0ea509351f4d46d1f Mon Sep 17 00:00:00 2001 From: Alexis Date: Mon, 10 Jun 2024 17:57:58 +0200 Subject: [PATCH 4/4] Remove unused import --- tests/e2e/detectors/test_detectors.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/e2e/detectors/test_detectors.py b/tests/e2e/detectors/test_detectors.py index 83931c32b6..c4fe2b14e5 100644 --- a/tests/e2e/detectors/test_detectors.py +++ b/tests/e2e/detectors/test_detectors.py @@ -1,5 +1,4 @@ import os -import shutil from pathlib import Path import sys from typing import Type, Optional, List