From d35714977be81807c50dc727864e66aefb050bcd Mon Sep 17 00:00:00 2001 From: xiyao Date: Tue, 6 Feb 2024 22:13:04 +0800 Subject: [PATCH 1/4] feat: add detector RugHook (prototype) --- .../uniswap_hook/uniswap_rug_hook.py | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 hookscan/detectors/uniswap_hook/uniswap_rug_hook.py diff --git a/hookscan/detectors/uniswap_hook/uniswap_rug_hook.py b/hookscan/detectors/uniswap_hook/uniswap_rug_hook.py new file mode 100644 index 0000000..05742ab --- /dev/null +++ b/hookscan/detectors/uniswap_hook/uniswap_rug_hook.py @@ -0,0 +1,54 @@ +# NOTE THIS IS PROTOTYPE + +from typing import Dict, List + +from hookscan.components.constant import ConstantInt +from hookscan.components.evm_instructions import Call, Callcode +from hookscan.components.value import Value +from hookscan.core.instruction_instance import ValueInstance +from hookscan.core.traversal_info import TraversalInfo +from hookscan.detectors.base_detector import BaseDetector +from hookscan.detectors.detector_result import DetectorResult + + +class UniswapRugHook(BaseDetector): + """Transfer to a controlled (by owner) address in a privilege function.""" + + VULNERABILITY_DESCRIPTION = "possible rug-pull hook" + + def __init__(self) -> None: + super().__init__() + self._result: Dict[Value, DetectorResult] = {} + self.callback_keys = ( + Call, + Callcode, + ) + + def callback(self, info: TraversalInfo, inst_instance: ValueInstance, is_end: bool): + if not info.function.is_runtime: + return + if is_end: + if not info.is_protected: + return # rug function should be privileged + for transfer_inst in self.get_all_hooked_instances(info): + if isinstance(transfer_inst_value := transfer_inst.value, (Call, Callcode)): + if self.get_call_signature(transfer_inst) in { # token transfer + 0xA9059CBB, # erc20 transfer(address,uint256) + 0x23B872DD, # erc20 transferFrom(address,address,uint256) + 0x42842E0E, # erc721 safeTransferFrom(address,address,uint256) + 0xB88D4FDE, # erc721 safeTransferFrom(address,address,uint256,bytes) + 0x23B872DD, # erc721 transferFrom(address,address,uint256) + 0x2EB2C2D6, # erc1155 safeBatchTransferFrom(address,address,uint256[],uint256[],bytes) + 0xF242432A, # erc1155 safeTransferFrom(address,address,uint256,uint256,bytes) + } or not ( # native call: value == 0 + isinstance(call_value := transfer_inst_value.operands[2], ConstantInt) and call_value.value == 0 + ): + self._result[transfer_inst_value] = DetectorResult( + target=inst_instance, severity="medium", confidence="medium" + ) + + def get_internal_result(self) -> List[DetectorResult]: + return list(self._result.values()) + + def get_external_result(self) -> List[DetectorResult]: + return self.get_internal_result() From 68dd3d98ef932a15fa07037dd8491365ed44d72a Mon Sep 17 00:00:00 2001 From: xiyao Date: Tue, 6 Feb 2024 22:19:34 +0800 Subject: [PATCH 2/4] fix: call_value trace def-source --- hookscan/detectors/uniswap_hook/uniswap_rug_hook.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/hookscan/detectors/uniswap_hook/uniswap_rug_hook.py b/hookscan/detectors/uniswap_hook/uniswap_rug_hook.py index 05742ab..763a662 100644 --- a/hookscan/detectors/uniswap_hook/uniswap_rug_hook.py +++ b/hookscan/detectors/uniswap_hook/uniswap_rug_hook.py @@ -32,7 +32,7 @@ def callback(self, info: TraversalInfo, inst_instance: ValueInstance, is_end: bo return # rug function should be privileged for transfer_inst in self.get_all_hooked_instances(info): if isinstance(transfer_inst_value := transfer_inst.value, (Call, Callcode)): - if self.get_call_signature(transfer_inst) in { # token transfer + if self.get_call_signature(transfer_inst) in { # token transfer, amount == 0 not considered 0xA9059CBB, # erc20 transfer(address,uint256) 0x23B872DD, # erc20 transferFrom(address,address,uint256) 0x42842E0E, # erc721 safeTransferFrom(address,address,uint256) @@ -41,7 +41,8 @@ def callback(self, info: TraversalInfo, inst_instance: ValueInstance, is_end: bo 0x2EB2C2D6, # erc1155 safeBatchTransferFrom(address,address,uint256[],uint256[],bytes) 0xF242432A, # erc1155 safeTransferFrom(address,address,uint256,uint256,bytes) } or not ( # native call: value == 0 - isinstance(call_value := transfer_inst_value.operands[2], ConstantInt) and call_value.value == 0 + isinstance(call_value := transfer_inst.operands[2].origin.value, ConstantInt) + and call_value.value == 0 ): self._result[transfer_inst_value] = DetectorResult( target=inst_instance, severity="medium", confidence="medium" From 0891d01fa09e1bd2685d96e40ff0a55d7b6461b2 Mon Sep 17 00:00:00 2001 From: xiyao Date: Fri, 1 Mar 2024 14:51:13 +0800 Subject: [PATCH 3/4] feat: detector uniswap-rug-hook --- hookscan/detectors/all_detectors.py | 2 + hookscan/detectors/base_detector.py | 24 ++ .../uniswap_hook/uniswap_rug_hook.py | 6 +- tests/unsafe/UniswapRugHook.sol | 258 ++++++++++++++++++ 4 files changed, 287 insertions(+), 3 deletions(-) create mode 100644 tests/unsafe/UniswapRugHook.sol diff --git a/hookscan/detectors/all_detectors.py b/hookscan/detectors/all_detectors.py index 4634c35..33daabf 100644 --- a/hookscan/detectors/all_detectors.py +++ b/hookscan/detectors/all_detectors.py @@ -2,6 +2,7 @@ from hookscan.detectors.uniswap_hook.uniswap_public_callback import UniswapPublicCallback from hookscan.detectors.uniswap_hook.uniswap_public_hook import UniswapPublicHook +from hookscan.detectors.uniswap_hook.uniswap_rug_hook import UniswapRugHook from hookscan.detectors.uniswap_hook.uniswap_suicidal_hook import UniswapSuicidalHook from hookscan.detectors.uniswap_hook.uniswap_upgradable_hook import UniswapUpgradableHook @@ -10,6 +11,7 @@ UniswapPublicHook, UniswapSuicidalHook, UniswapUpgradableHook, + UniswapRugHook, ] all_detectors_dict: Dict[str, type] = {detector.__name__: detector for detector in all_detectors} diff --git a/hookscan/detectors/base_detector.py b/hookscan/detectors/base_detector.py index 65c08e6..a151c05 100644 --- a/hookscan/detectors/base_detector.py +++ b/hookscan/detectors/base_detector.py @@ -194,3 +194,27 @@ def taint_in( elif taint == target_taint: return True return False + + def get_all_hooked_instances( + self, info: "TraversalInfo", target_inst_types: Optional[List[Type[Instruction]]] = None + ) -> List[ValueInstance]: + def valid_target(target_inst_types: List[Type[Instruction]]): + for key1 in target_inst_types: + for key2 in target_inst_types: + if key1 == key2: + continue + assert not (issubclass(key1, key2) or issubclass(key2, key1)) + + hooked_instances = [] + inst_types = target_inst_types if target_inst_types is not None else list(self.callback_keys) + + valid_target(inst_types) + + for path_index, inst_index in info.trigger_index_list: + path_node = info.path[path_index] + inst_index -= path_node.start_index + inst_instance = path_node.inst_instances[inst_index] + for consider_inst_type in inst_types: + if isinstance(inst_instance.value, consider_inst_type): + hooked_instances.append(inst_instance) + return hooked_instances diff --git a/hookscan/detectors/uniswap_hook/uniswap_rug_hook.py b/hookscan/detectors/uniswap_hook/uniswap_rug_hook.py index 763a662..670aac8 100644 --- a/hookscan/detectors/uniswap_hook/uniswap_rug_hook.py +++ b/hookscan/detectors/uniswap_hook/uniswap_rug_hook.py @@ -40,12 +40,12 @@ def callback(self, info: TraversalInfo, inst_instance: ValueInstance, is_end: bo 0x23B872DD, # erc721 transferFrom(address,address,uint256) 0x2EB2C2D6, # erc1155 safeBatchTransferFrom(address,address,uint256[],uint256[],bytes) 0xF242432A, # erc1155 safeTransferFrom(address,address,uint256,uint256,bytes) - } or not ( # native call: value == 0 - isinstance(call_value := transfer_inst.operands[2].origin.value, ConstantInt) + } or not ( # native call: msg.value != 0 + isinstance(call_value := transfer_inst.operand_instances[2].origin.value, ConstantInt) and call_value.value == 0 ): self._result[transfer_inst_value] = DetectorResult( - target=inst_instance, severity="medium", confidence="medium" + target=transfer_inst, severity="high", confidence="medium" ) def get_internal_result(self) -> List[DetectorResult]: diff --git a/tests/unsafe/UniswapRugHook.sol b/tests/unsafe/UniswapRugHook.sol new file mode 100644 index 0000000..120c61b --- /dev/null +++ b/tests/unsafe/UniswapRugHook.sol @@ -0,0 +1,258 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.1; + +type Currency is address; +type BalanceDelta is int256; + +struct PoolKey { + Currency currency0; + Currency currency1; + // address currency0; + // address currency1; + uint24 fee; + int24 tickSpacing; + IHooks hooks; +} + +interface IPoolManager { + struct ModifyPositionParams { + int24 tickLower; + int24 tickUpper; + int256 liquidityDelta; + } + + struct SwapParams { + bool zeroForOne; + int256 amountSpecified; + uint160 sqrtPriceLimitX96; + } + + function lock(bytes calldata data) external returns (bytes memory); +} + +interface IHooks { + function beforeInitialize( + address sender, + PoolKey calldata key, + uint160 sqrtPriceX96, + bytes calldata hookData + ) external returns (bytes4); + + function afterInitialize( + address sender, + PoolKey calldata key, + uint160 sqrtPriceX96, + int24 tick, + bytes calldata hookData + ) external returns (bytes4); + + function beforeModifyPosition( + address sender, + PoolKey calldata key, + IPoolManager.ModifyPositionParams calldata params, + bytes calldata hookData + ) external returns (bytes4); + + function afterModifyPosition( + address sender, + PoolKey calldata key, + IPoolManager.ModifyPositionParams calldata params, + BalanceDelta delta, + // int256 delta, + bytes calldata hookData + ) external returns (bytes4); + + function beforeSwap( + address sender, + PoolKey calldata key, + IPoolManager.SwapParams calldata params, + bytes calldata hookData + ) external returns (bytes4); + + function afterSwap( + address sender, + PoolKey calldata key, + IPoolManager.SwapParams calldata params, + BalanceDelta delta, + // int256 delta, + bytes calldata hookData + ) external returns (bytes4); + + function beforeDonate( + address sender, + PoolKey calldata key, + uint256 amount0, + uint256 amount1, + bytes calldata hookData + ) external returns (bytes4); + + function afterDonate( + address sender, + PoolKey calldata key, + uint256 amount0, + uint256 amount1, + bytes calldata hookData + ) external returns (bytes4); +} + +abstract contract BaseHook is IHooks { + error NotPoolManager(); + error NotSelf(); + error InvalidPool(); + error LockFailure(); + error HookNotImplemented(); + + IPoolManager public immutable poolManager; + + constructor(IPoolManager _poolManager) { + poolManager = _poolManager; + } + + modifier poolManagerOnly() { + if (msg.sender != address(poolManager)) revert NotPoolManager(); + _; + } + + modifier selfOnly() { + if (msg.sender != address(this)) revert NotSelf(); + _; + } + + modifier onlyValidPools(IHooks hooks) { + if (hooks != this) revert InvalidPool(); + _; + } + + function lockAcquired( + bytes calldata data + ) external virtual poolManagerOnly returns (bytes memory) { + (bool success, bytes memory returnData) = address(this).call(data); + if (success) return returnData; + if (returnData.length == 0) revert LockFailure(); + assembly { + revert(add(returnData, 32), mload(returnData)) + } + } + + function beforeInitialize( + address, + PoolKey calldata, + uint160, + bytes calldata + ) external virtual returns (bytes4) { + revert HookNotImplemented(); + } + + function afterInitialize( + address, + PoolKey calldata, + uint160, + int24, + bytes calldata + ) external virtual returns (bytes4) { + revert HookNotImplemented(); + } + + function beforeModifyPosition( + address, + PoolKey calldata, + IPoolManager.ModifyPositionParams calldata, + bytes calldata + ) external virtual returns (bytes4) { + revert HookNotImplemented(); + } + + function afterModifyPosition( + address, + PoolKey calldata, + IPoolManager.ModifyPositionParams calldata, + BalanceDelta, + // int256, + bytes calldata + ) external virtual returns (bytes4) { + revert HookNotImplemented(); + } + + function beforeSwap( + address, + PoolKey calldata, + IPoolManager.SwapParams calldata, + bytes calldata + ) external virtual returns (bytes4) { + revert HookNotImplemented(); + } + + function afterSwap( + address, + PoolKey calldata, + IPoolManager.SwapParams calldata, + BalanceDelta, + // int256, + bytes calldata + ) external virtual returns (bytes4) { + revert HookNotImplemented(); + } + + function beforeDonate( + address, + PoolKey calldata, + uint256, + uint256, + bytes calldata + ) external virtual returns (bytes4) { + revert HookNotImplemented(); + } + + function afterDonate( + address, + PoolKey calldata, + uint256, + uint256, + bytes calldata + ) external virtual returns (bytes4) { + revert HookNotImplemented(); + } +} + +interface Token { + function transfer(address, uint256) external returns (bool); + + function balanceOf(address) external view returns (uint256); +} + +contract Hook is BaseHook { + uint count; + address owner; + Token token; + + constructor( + IPoolManager _poolManager, + address _owner, + address _token + ) BaseHook(_poolManager) { + owner = _owner; + token = Token(_token); + } + + function beforeSwap( + address, + PoolKey calldata, + IPoolManager.SwapParams calldata, + bytes calldata + ) external override poolManagerOnly returns (bytes4) { + count++; + return IHooks.beforeSwap.selector; + } + + // ... functions including logic of receiving ether and tokens + + function clear_token() external { + require(msg.sender == owner, "owner only"); + token.transfer(owner, token.balanceOf(owner)); + } + + function clear_eth() external { + require(msg.sender == owner, "owner only"); + payable(owner).transfer(address(this).balance); + } +} From 0829ce5b4261982ad1f884629d40e73292d2cc6d Mon Sep 17 00:00:00 2001 From: xiyao Date: Mon, 4 Mar 2024 11:59:21 +0800 Subject: [PATCH 4/4] docs: UniswapRugHook --- README.md | 3 +++ docs/detectors/UniswapRugHook.md | 12 ++++++++++++ 2 files changed, 15 insertions(+) create mode 100644 docs/detectors/UniswapRugHook.md diff --git a/README.md b/README.md index 2dd48ed..98ebc46 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,7 @@ docker run --rm -it \ | [`UniswapPublicCallback`][public_callback] | callers of callback functions are not exclusively
restricted to the contract itself | High | High | | [`UniswapUpgradableHook`][upgradable_hook] | the contract `DELEGATECALL`s to mutable addresses | High | High | | [`UniswapSuicidalHook`][suicidal_hook] | the contract contains `SELFDESTRUCT` | Medium | High | +| [`UniswapRugHook`][rug_hook] | the hook is a possible rug pull contract | High | Medium | ## Evaluation @@ -94,6 +95,7 @@ The test results are as follows: | [`UniswapPublicCallback`][public_callback] | 3/3 contracts | | [`UniswapUpgradableHook`][upgradable_hook] | 0 | | [`UniswapSuicidalHook`][suicidal_hook] | 0 | +| [`UniswapRugHook`][rug_hook] | N/A | ## Note @@ -111,3 +113,4 @@ This project is under the AGPLv3 License. See the LICENSE file for the full lice [public_hook]: docs/detectors/UniswapPublicHook.md [upgradable_hook]: docs/detectors/UniswapUpgradableHook.md [suicidal_hook]: docs/detectors/UniswapSuicidalHook.md +[rug_hook]: docs/detectors/UniswapRugHook.md diff --git a/docs/detectors/UniswapRugHook.md b/docs/detectors/UniswapRugHook.md new file mode 100644 index 0000000..341174a --- /dev/null +++ b/docs/detectors/UniswapRugHook.md @@ -0,0 +1,12 @@ +# UniswapSuicidalHook + +## Info + +**Spec** + +- Severity: High +- Confidence: Medium + +**Description** + +Find potential rug pull contracts by detecting instances where native tokens or ERC tokens are transferred to an address controlled by the owner within a privileged function.