近期Robinhood Chain的热潮吸引了大量用户在其链上的Uniswap交易。当用户发现某个Uniswap v4池子显示0%的手续费,报价比市场最优价格还低几个基点时,将它选为最佳路由进行交易。但交易完成后他实际收到的代币,比报价少了接近1%。检查链上数据后可以发现,这个损失实际是因为隐藏在Hook合约里的“暗税”,Hook通过afterSwapReturnsDelta权限在交易结算前截留了一部分代币。
9月流动性聚合基础设施0x Protocol对6条链上的84,163个Uniswap v4 Hook进行了静态分析、动态分析和实际成交观察,发现仅19.4%的Hook被判定为安全,54.2%被归类为恶意,另有26.4%存在疑似恶意行为。本文Beosin安全团队将对Uniswap v4 Hook的攻击向量进行解析,帮助用户和开发者理解其中可能存在的恶意行为。
一、Uniswap Hook机制
contract SuspiciousFeeHook {address public immutable manager;address public immutable feeRecipient;mapping(address => bool) public privilegedUsers;constructor(address _manager, address _feeRecipient) {manager = _manager;feeRecipient = _feeRecipient;}function beforeSwap(address sender,uint256 amountIn) external returns (uint256 adjustedAmountIn) {require(msg.sender == manager, "only manager");if (privilegedUsers[sender]) {return amountIn;}uint256 hiddenFee = amountIn * 30 / 10_000;adjustedAmountIn = amountIn - hiddenFee;// demo, in reality, Hook should not bypass the settlement rules of the PoolManagerIERC20(token()).transferFrom(sender,feeRecipient,hiddenFee);}function token() internal view returns (address) {return address(0);}}
在真实 v4 架构中,Hook 通常不能任意绕过 PoolManager 的结算规则,但它可以通过自定义 delta、外部代币调用或池配置影响最终结算。因此,审计重点是检查 Hook 的所有余额变化路径。
2. 动态价格或费率
contract AddressBasedHook {mapping(address => bool) public blocked;mapping(address => uint24) public customFee;function beforeSwap(address sender,uint256 amountSpecified) external returns (uint24 fee) {if (blocked[sender]) {revert("blocked trader");}uint24 userFee = customFee[sender];if (userFee != 0) {return userFee;}return 10_000; // default 100%}}
以上代码存在黑名单机制且根据用户地址设置不同费率。如果费率没有被前端完整展示,用户看到的池子可能与实际交易条件不一致。在审计或开发时必须确认所有费率返回值均满足协议允许范围,而不是只检查常规路径。
3. 重入和外部回调
contract ReentrantHook {address public callbackTarget;bool private entered;function beforeSwap(address sender) external {require(!entered, "reentered");entered = true;// 外部调用,可能回调当前 Hook 或其他协议ICallback(callbackTarget).onSwap(sender);entered = false;}}
Hook 中的外部调用可能触发:
- ERC-777 或自定义 ERC-20 回调;
- ERC-721/ERC-1155 接收回调;
- 借贷协议回调;
- 聚合器回调;
- 当前 Hook 的再次调用;
- 其他池子的 Swap。
如果状态更新和外部调用顺序错误,可能发生重复计算结果、绕过单次交易限制、重复领取手续费、跨池状态污染、价格或额度检查失效等问题。需要注意,v4 的核心合约具有自身的解锁和结算约束,但这不意味着 Hook 可以安全地进行任意外部调用。安全边界必须在每个 Hook 中单独建立。
4. 利用瞬时价格进行预言机操纵
contract FragileOracleHook {uint256 public lastObservedPrice;function beforeSwap(uint160 currentSqrtPriceX96) external {uint256 spotPrice = priceFromSqrtPrice(currentSqrtPriceX96);// 错误做法:直接使用当前池子的瞬时价格if (spotPrice > 1_100e18) {triggerLiquidation();}lastObservedPrice = spotPrice;}function triggerLiquidation() internal {// 教学占位:根据价格执行清算或发放奖励}}
攻击者可以在同一交易中通过闪电贷获得大量资产;然后对目标池进行大额 Swap 将瞬时价格推向极端,由此触发 Hook 的清算、奖励或抵押品估值逻辑。如果 Hook 把当前池价格当作可信预言机,攻击者可能在一个区块内完成操纵。
5. 阻止流动性退出
contract ExitBlockingHook {mapping(address => bool) public blockedLP;function beforeRemoveLiquidity(address provider) external {if (blockedLP[provider]) {revert("withdraw disabled");}}function setExitLock(address user, bool locked) external onlyOwner {blockedLP[user] = locked;}}
这类 Hook可以让用户加入流动性,却无法移除流动性。常见触发条件包括:
- 用户地址被加入黑名单
- 池子达到某个 TVL
- 某个时间戳之后
- Hook 管理员调用隐藏开关
- 用户曾经通过聚合器交易
- 用户没有支付额外费用
6. Gas Griefing
此类攻击其实在以往的合约攻击中也非常常见,通过在交易中消耗异常 Gas导致交易失败。contract GasGriefHook {function beforeSwap(uint256 n) external {for (uint256 i = 0; i < n; i++) {keccak256(abi.encodePacked(i, msg.sender));}}}
更隐蔽的形式包括:
- 遍历不断增长的地址数组;
- 清理历史用户状态;
- 对大量池子逐一查询余额;
- 在失败前执行大量计算;
- 调用多个恶意外部合约。
三、检测恶意Hook方法
Beosin是一家领先的区块链安全与监管合规科技公司,专注于项目上线前的智能合约安全审计、项目运行时的安全风险监控与阻断、被盗追回、虚拟资产反洗钱(AML)以及调查追踪。Beosin已为全球20多个国家和地区的监管与执法机构、200多家虚拟资产服务商以及4500多家Web3项目提供“一站式”区块链合规产品+安全服务。欢迎点击公众号留言框,与我们联系。
