Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add detector RugHook (prototype) #13

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ docker run --rm -it \
| [`UniswapPublicCallback`][public_callback] | callers of callback functions are not exclusively<br />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

Expand All @@ -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

Expand All @@ -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
12 changes: 12 additions & 0 deletions docs/detectors/UniswapRugHook.md
Original file line number Diff line number Diff line change
@@ -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.
2 changes: 2 additions & 0 deletions hookscan/detectors/all_detectors.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -10,6 +11,7 @@
UniswapPublicHook,
UniswapSuicidalHook,
UniswapUpgradableHook,
UniswapRugHook,
]

all_detectors_dict: Dict[str, type] = {detector.__name__: detector for detector in all_detectors}
24 changes: 24 additions & 0 deletions hookscan/detectors/base_detector.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
55 changes: 55 additions & 0 deletions hookscan/detectors/uniswap_hook/uniswap_rug_hook.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# 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, amount == 0 not considered
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: 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=transfer_inst, severity="high", 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()
258 changes: 258 additions & 0 deletions tests/unsafe/UniswapRugHook.sol
Original file line number Diff line number Diff line change
@@ -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);
}
}
Loading