main 1fbf95547791 cached
21 files
53.1 KB
13.3k tokens
1 requests
Download .txt
Repository: uniswapfoundation/v4-template
Branch: main
Commit: 1fbf95547791
Files: 21
Total size: 53.1 KB

Directory structure:
gitextract_c36u9obz/

├── .github/
│   └── workflows/
│       └── test.yml
├── .gitignore
├── .gitmodules
├── .vscode/
│   └── settings.json
├── LICENSE
├── README.md
├── foundry.toml
├── remappings.txt
├── script/
│   ├── 00_DeployHook.s.sol
│   ├── 01_CreatePoolAndAddLiquidity.s.sol
│   ├── 02_AddLiquidity.s.sol
│   ├── 03_Swap.s.sol
│   ├── base/
│   │   ├── BaseScript.sol
│   │   └── LiquidityHelpers.sol
│   └── testing/
│       └── 00_DeployV4.s.sol
├── src/
│   └── Counter.sol
└── test/
    ├── Counter.t.sol
    └── utils/
        ├── BaseTest.sol
        ├── Deployers.sol
        └── libraries/
            ├── EasyPosm.sol
            └── EasyPosm.t.sol

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/workflows/test.yml
================================================
name: Test Suite

on:
  workflow_dispatch:
  # push:
  # pull_request:

env:
  FOUNDRY_PROFILE: ci

jobs:
  test:
    name: Foundry Project
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          submodules: recursive

      - name: Install Foundry
        uses: foundry-rs/foundry-toolchain@v1
        with:
          version: stable

      - name: Build
        run: |
          forge --version
          forge build --sizes
        id: build

      - name: Test
        run: |
          forge test -vvv
        id: test


================================================
FILE: .gitignore
================================================
# Compiler files
cache/
out/

# Ignores development (and goerli) broadcast logs
!/broadcast
/broadcast/*/31337/
/broadcast/*/5/
/broadcast/**/dry-run/

# Docs
docs/

# Dotenv file
.env

# Node modules
node_modules/

================================================
FILE: .gitmodules
================================================
[submodule "lib/forge-std"]
	path = lib/forge-std
	url = https://github.com/foundry-rs/forge-std
[submodule "lib/uniswap-hooks"]
	path = lib/uniswap-hooks
	url = https://github.com/openzeppelin/uniswap-hooks
[submodule "lib/hookmate"]
	path = lib/hookmate
	url = https://github.com/akshatmittal/hookmate

================================================
FILE: .vscode/settings.json
================================================
{
  "solidity.formatter": "forge"
}


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2023 saucepoint

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
# Uniswap v4 Hook Template

**A template for writing Uniswap v4 Hooks 🦄**

### Get Started

This template provides a starting point for writing Uniswap v4 Hooks, including a simple example and preconfigured test environment. Start by creating a new repository using the "Use this template" button at the top right of this page. Alternatively you can also click this link:

[![Use this Template](https://img.shields.io/badge/Use%20this%20Template-101010?style=for-the-badge&logo=github)](https://github.com/uniswapfoundation/v4-template/generate)

1. The example hook [Counter.sol](src/Counter.sol) demonstrates the `beforeSwap()` and `afterSwap()` hooks
2. The test template [Counter.t.sol](test/Counter.t.sol) preconfigures the v4 pool manager, test tokens, and test liquidity.

<details>
<summary>Updating to v4-template:latest</summary>

This template is actively maintained -- you can update the v4 dependencies, scripts, and helpers:

```bash
git remote add template https://github.com/uniswapfoundation/v4-template
git fetch template
git merge template/main <BRANCH> --allow-unrelated-histories
```

</details>

### Requirements

This template is designed to work with Foundry (stable). If you are using Foundry Nightly, you may encounter compatibility issues. You can update your Foundry installation to the latest stable version by running:

```
foundryup
```

To set up the project, run the following commands in your terminal to install dependencies and run the tests:

```
forge install
forge test
```

### Local Development

Other than writing unit tests (recommended!), you can only deploy & test hooks on [anvil](https://book.getfoundry.sh/anvil/) locally. Scripts are available in the `script/` directory, which can be used to deploy hooks, create pools, provide liquidity and swap tokens. The scripts support both local `anvil` environment as well as running them directly on a production network.

### Executing locally with using **Anvil**:

1. Start Anvil (or fork a specific chain using anvil):

```bash
anvil
```

or

```bash
anvil --fork-url <YOUR_RPC_URL>
```

2. Execute scripts:

```bash
forge script script/00_DeployHook.s.sol \
    --rpc-url http://localhost:8545 \
    --private-key <PRIVATE_KEY> \
    --broadcast
```

### Using **RPC URLs** (actual transactions):

:::info
It is best to not store your private key even in .env or enter it directly in the command line. Instead use the `--account` flag to select your private key from your keystore.
:::

### Follow these steps if you have not stored your private key in the keystore:

<details>

1. Add your private key to the keystore:

```bash
cast wallet import <SET_A_NAME_FOR_KEY> --interactive
```

2. You will prompted to enter your private key and set a password, fill and press enter:

```
Enter private key: <YOUR_PRIVATE_KEY>
Enter keystore password: <SET_NEW_PASSWORD>
```

You should see this:

```
`<YOUR_WALLET_PRIVATE_KEY_NAME>` keystore was saved successfully. Address: <YOUR_WALLET_ADDRESS>
```

::: warning
Use `history -c` to clear your command history.
:::

</details>

1. Execute scripts:

```bash
forge script script/00_DeployHook.s.sol \
    --rpc-url <YOUR_RPC_URL> \
    --account <YOUR_WALLET_PRIVATE_KEY_NAME> \
    --sender <YOUR_WALLET_ADDRESS> \
    --broadcast
```

You will prompted to enter your wallet password, fill and press enter:

```
Enter keystore password: <YOUR_PASSWORD>
```

### Key Modifications to note:

1. Update the `token0` and `token1` addresses in the `BaseScript.sol` file to match the tokens you want to use in the network of your choice for sepolia and mainnet deployments.
2. Update the `token0Amount` and `token1Amount` in the `CreatePoolAndAddLiquidity.s.sol` file to match the amount of tokens you want to provide liquidity with.
3. Update the `token0Amount` and `token1Amount` in the `AddLiquidity.s.sol` file to match the amount of tokens you want to provide liquidity with.
4. Update the `amountIn` and `amountOutMin` in the `Swap.s.sol` file to match the amount of tokens you want to swap.

### Verifying the hook contract

```bash
forge verify-contract \
  --rpc-url <URL> \
  --chain <CHAIN_NAME_OR_ID> \
  # Generally etherscan
  --verifier <Verification_Provider> \
  # Use --etherscan-api-key <ETHERSCAN_API_KEY> if you are using etherscan
  --verifier-api-key <Verification_Provider_API_KEY> \
  --constructor-args <ABI_ENCODED_ARGS> \
  --num-of-optimizations <OPTIMIZER_RUNS> \
  <Contract_Address> \
  <path/to/Contract.sol:ContractName>
  --watch
```

### Troubleshooting

<details>

#### Permission Denied

When installing dependencies with `forge install`, Github may throw a `Permission Denied` error

Typically caused by missing Github SSH keys, and can be resolved by following the steps [here](https://docs.github.com/en/github/authenticating-to-github/connecting-to-github-with-ssh)

Or [adding the keys to your ssh-agent](https://docs.github.com/en/authentication/connecting-to-github-with-ssh/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent#adding-your-ssh-key-to-the-ssh-agent), if you have already uploaded SSH keys

#### Anvil fork test failures

Some versions of Foundry may limit contract code size to ~25kb, which could prevent local tests to fail. You can resolve this by setting the `code-size-limit` flag

```
anvil --code-size-limit 40000
```

#### Hook deployment failures

Hook deployment failures are caused by incorrect flags or incorrect salt mining

1. Verify the flags are in agreement:
   - `getHookCalls()` returns the correct flags
   - `flags` provided to `HookMiner.find(...)`
2. Verify salt mining is correct:
   - In **forge test**: the _deployer_ for: `new Hook{salt: salt}(...)` and `HookMiner.find(deployer, ...)` are the same. This will be `address(this)`. If using `vm.prank`, the deployer will be the pranking address
   - In **forge script**: the deployer must be the CREATE2 Proxy: `0x4e59b44847b379578588920cA78FbF26c0B4956C`
     - If anvil does not have the CREATE2 deployer, your foundry may be out of date. You can update it with `foundryup`

</details>

### Additional Resources

- [Uniswap v4 docs](https://docs.uniswap.org/contracts/v4/overview)
- [v4-periphery](https://github.com/uniswap/v4-periphery)
- [v4-core](https://github.com/uniswap/v4-core)
- [v4-by-example](https://v4-by-example.org)


================================================
FILE: foundry.toml
================================================
[profile.default]
bytecode_hash = "none"
evm_version = "cancun"
ffi = true
fs_permissions = [{access = "read-write", path = ".forge-snapshots/"}]
libs = ["lib"]
out = "out"
solc_version = "0.8.30"
src = "src"
via_ir = false

[lint]
exclude_lints = ["screaming-snake-case-immutable", "screaming-snake-case-const"]
lint_on_build = false


================================================
FILE: remappings.txt
================================================
forge-std/=lib/forge-std/src/

@uniswap/v4-core/=lib/uniswap-hooks/lib/v4-core/
@uniswap/v4-periphery/=lib/uniswap-hooks/lib/v4-periphery/
v4-core/=lib/uniswap-hooks/lib/v4-core/
v4-periphery/=lib/uniswap-hooks/lib/v4-periphery/

@openzeppelin/uniswap-hooks/=lib/uniswap-hooks/
@openzeppelin/contracts/=lib/uniswap-hooks/lib/v4-core/lib/openzeppelin-contracts/contracts/

hookmate/=lib/hookmate/src/
permit2/=lib/uniswap-hooks/lib/v4-periphery/lib/permit2/
solmate/=lib/uniswap-hooks/lib/v4-core/lib/solmate/

================================================
FILE: script/00_DeployHook.s.sol
================================================
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol";
import {HookMiner} from "@uniswap/v4-periphery/src/utils/HookMiner.sol";

import {BaseScript} from "./base/BaseScript.sol";

import {Counter} from "../src/Counter.sol";

/// @notice Mines the address and deploys the Counter.sol Hook contract
contract DeployHookScript is BaseScript {
    function run() public {
        // hook contracts must have specific flags encoded in the address
        uint160 flags = uint160(
            Hooks.BEFORE_SWAP_FLAG | Hooks.AFTER_SWAP_FLAG | Hooks.BEFORE_ADD_LIQUIDITY_FLAG
                | Hooks.BEFORE_REMOVE_LIQUIDITY_FLAG
        );

        // Mine a salt that will produce a hook address with the correct flags
        bytes memory constructorArgs = abi.encode(poolManager);
        (address hookAddress, bytes32 salt) =
            HookMiner.find(CREATE2_FACTORY, flags, type(Counter).creationCode, constructorArgs);

        // Deploy the hook using CREATE2
        vm.startBroadcast();
        Counter counter = new Counter{salt: salt}(poolManager);
        vm.stopBroadcast();

        require(address(counter) == hookAddress, "DeployHookScript: Hook Address Mismatch");
    }
}


================================================
FILE: script/01_CreatePoolAndAddLiquidity.s.sol
================================================
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
import {CurrencyLibrary, Currency} from "@uniswap/v4-core/src/types/Currency.sol";
import {LiquidityAmounts} from "@uniswap/v4-core/test/utils/LiquidityAmounts.sol";
import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol";

import {BaseScript} from "./base/BaseScript.sol";
import {LiquidityHelpers} from "./base/LiquidityHelpers.sol";

contract CreatePoolAndAddLiquidityScript is BaseScript, LiquidityHelpers {
    using CurrencyLibrary for Currency;

    /////////////////////////////////////
    // --- Configure These ---
    /////////////////////////////////////

    uint24 lpFee = 3000; // 0.30%
    int24 tickSpacing = 60;
    uint160 startingPrice = 2 ** 96; // Starting price, sqrtPriceX96; floor(sqrt(1) * 2^96)

    // --- liquidity position configuration --- //
    uint256 public token0Amount = 100e18;
    uint256 public token1Amount = 100e18;

    // range of the position, must be a multiple of tickSpacing
    int24 tickLower;
    int24 tickUpper;
    /////////////////////////////////////

    function run() external {
        PoolKey memory poolKey = PoolKey({
            currency0: currency0,
            currency1: currency1,
            fee: lpFee,
            tickSpacing: tickSpacing,
            hooks: hookContract
        });

        bytes memory hookData = new bytes(0);

        int24 currentTick = TickMath.getTickAtSqrtPrice(startingPrice);

        tickLower = truncateTickSpacing((currentTick - 750 * tickSpacing), tickSpacing);
        tickUpper = truncateTickSpacing((currentTick + 750 * tickSpacing), tickSpacing);

        // Converts token amounts to liquidity units
        uint128 liquidity = LiquidityAmounts.getLiquidityForAmounts(
            startingPrice,
            TickMath.getSqrtPriceAtTick(tickLower),
            TickMath.getSqrtPriceAtTick(tickUpper),
            token0Amount,
            token1Amount
        );

        // slippage limits
        uint256 amount0Max = token0Amount + 1;
        uint256 amount1Max = token1Amount + 1;

        (bytes memory actions, bytes[] memory mintParams) = _mintLiquidityParams(
            poolKey, tickLower, tickUpper, liquidity, amount0Max, amount1Max, deployerAddress, hookData
        );

        // multicall parameters
        bytes[] memory params = new bytes[](2);

        // Initialize Pool
        params[0] = abi.encodeWithSelector(positionManager.initializePool.selector, poolKey, startingPrice, hookData);

        // Mint Liquidity
        params[1] = abi.encodeWithSelector(
            positionManager.modifyLiquidities.selector, abi.encode(actions, mintParams), block.timestamp + 3600
        );

        // If the pool is an ETH pair, native tokens are to be transferred
        uint256 valueToPass = currency0.isAddressZero() ? amount0Max : 0;

        vm.startBroadcast();
        tokenApprovals();

        // Multicall to atomically create pool & add liquidity
        positionManager.multicall{value: valueToPass}(params);
        vm.stopBroadcast();
    }
}


================================================
FILE: script/02_AddLiquidity.s.sol
================================================
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
import {CurrencyLibrary, Currency} from "@uniswap/v4-core/src/types/Currency.sol";
import {LiquidityAmounts} from "@uniswap/v4-core/test/utils/LiquidityAmounts.sol";
import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol";
import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol";

import {BaseScript} from "./base/BaseScript.sol";
import {LiquidityHelpers} from "./base/LiquidityHelpers.sol";

contract AddLiquidityScript is BaseScript, LiquidityHelpers {
    using CurrencyLibrary for Currency;
    using StateLibrary for IPoolManager;

    /////////////////////////////////////
    // --- Configure These ---
    /////////////////////////////////////

    uint24 lpFee = 3000; // 0.30%
    int24 tickSpacing = 60;

    // --- liquidity position configuration --- //
    uint256 public token0Amount = 1e18;
    uint256 public token1Amount = 1e18;

    /////////////////////////////////////

    int24 tickLower;
    int24 tickUpper;

    function run() external {
        PoolKey memory poolKey = PoolKey({
            currency0: currency0,
            currency1: currency1,
            fee: lpFee,
            tickSpacing: tickSpacing,
            hooks: hookContract
        });
        bytes memory hookData = new bytes(0);

        (uint160 sqrtPriceX96,,,) = poolManager.getSlot0(poolKey.toId());

        int24 currentTick = TickMath.getTickAtSqrtPrice(sqrtPriceX96);

        tickLower = truncateTickSpacing((currentTick - 1000 * tickSpacing), tickSpacing);
        tickUpper = truncateTickSpacing((currentTick + 1000 * tickSpacing), tickSpacing);

        // Converts token amounts to liquidity units
        uint128 liquidity = LiquidityAmounts.getLiquidityForAmounts(
            sqrtPriceX96,
            TickMath.getSqrtPriceAtTick(tickLower),
            TickMath.getSqrtPriceAtTick(tickUpper),
            token0Amount,
            token1Amount
        );

        // slippage limits
        uint256 amount0Max = token0Amount + 1 wei;
        uint256 amount1Max = token1Amount + 1 wei;

        (bytes memory actions, bytes[] memory mintParams) = _mintLiquidityParams(
            poolKey, tickLower, tickUpper, liquidity, amount0Max, amount1Max, deployerAddress, hookData
        );

        // multicall parameters
        bytes[] memory params = new bytes[](1);

        // Mint Liquidity
        params[0] = abi.encodeWithSelector(
            positionManager.modifyLiquidities.selector, abi.encode(actions, mintParams), block.timestamp + 60
        );

        // If the pool is an ETH pair, native tokens are to be transferred
        uint256 valueToPass = currency0.isAddressZero() ? amount0Max : 0;

        vm.startBroadcast();
        tokenApprovals();

        // Add liquidity to existing pool
        positionManager.multicall{value: valueToPass}(params);
        vm.stopBroadcast();
    }
}


================================================
FILE: script/03_Swap.s.sol
================================================
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";

import {BaseScript} from "./base/BaseScript.sol";

contract SwapScript is BaseScript {
    function run() external {
        PoolKey memory poolKey = PoolKey({
            currency0: currency0,
            currency1: currency1,
            fee: 3000,
            tickSpacing: 60,
            hooks: hookContract // This must match the pool
        });
        bytes memory hookData = new bytes(0);

        vm.startBroadcast();

        // We'll approve both, just for testing.
        token1.approve(address(swapRouter), type(uint256).max);
        token0.approve(address(swapRouter), type(uint256).max);

        // Execute swap
        swapRouter.swapExactTokensForTokens({
            amountIn: 1e18,
            amountOutMin: 0, // Very bad, but we want to allow for unlimited price impact
            zeroForOne: true,
            poolKey: poolKey,
            hookData: hookData,
            receiver: address(this),
            deadline: block.timestamp + 30
        });

        vm.stopBroadcast();
    }
}


================================================
FILE: script/base/BaseScript.sol
================================================
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

import {Script} from "forge-std/Script.sol";
import {IERC20} from "forge-std/interfaces/IERC20.sol";

import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol";
import {Currency} from "@uniswap/v4-core/src/types/Currency.sol";
import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
import {IPositionManager} from "@uniswap/v4-periphery/src/interfaces/IPositionManager.sol";
import {IPermit2} from "permit2/src/interfaces/IPermit2.sol";

import {IUniswapV4Router04} from "hookmate/interfaces/router/IUniswapV4Router04.sol";
import {AddressConstants} from "hookmate/constants/AddressConstants.sol";

import {Deployers} from "test/utils/Deployers.sol";

/// @notice Shared configuration between scripts
contract BaseScript is Script, Deployers {
    address immutable deployerAddress;

    /////////////////////////////////////
    // --- Configure These ---
    /////////////////////////////////////
    IERC20 internal constant token0 = IERC20(0x0165878A594ca255338adfa4d48449f69242Eb8F);
    IERC20 internal constant token1 = IERC20(0xa513E6E4b8f2a923D98304ec87F64353C4D5C853);
    IHooks constant hookContract = IHooks(address(0));
    /////////////////////////////////////

    Currency immutable currency0;
    Currency immutable currency1;

    constructor() {
        // Make sure artifacts are available, either deploy or configure.
        deployArtifacts();

        deployerAddress = getDeployer();

        (currency0, currency1) = getCurrencies();

        vm.label(address(permit2), "Permit2");
        vm.label(address(poolManager), "V4PoolManager");
        vm.label(address(positionManager), "V4PositionManager");
        vm.label(address(swapRouter), "V4SwapRouter");

        vm.label(address(token0), "Currency0");
        vm.label(address(token1), "Currency1");

        vm.label(address(hookContract), "HookContract");
    }

    function _etch(address target, bytes memory bytecode) internal override {
        if (block.chainid == 31337) {
            vm.rpc("anvil_setCode", string.concat('["', vm.toString(target), '",', '"', vm.toString(bytecode), '"]'));
        } else {
            revert("Unsupported etch on this network");
        }
    }

    function getCurrencies() internal pure returns (Currency, Currency) {
        require(address(token0) != address(token1));

        if (token0 < token1) {
            return (Currency.wrap(address(token0)), Currency.wrap(address(token1)));
        } else {
            return (Currency.wrap(address(token1)), Currency.wrap(address(token0)));
        }
    }

    function getDeployer() internal returns (address) {
        address[] memory wallets = vm.getWallets();

        if (wallets.length > 0) {
            return wallets[0];
        } else {
            return msg.sender;
        }
    }
}


================================================
FILE: script/base/LiquidityHelpers.sol
================================================
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
import {CurrencyLibrary, Currency} from "@uniswap/v4-core/src/types/Currency.sol";
import {Actions} from "@uniswap/v4-periphery/src/libraries/Actions.sol";

import {BaseScript} from "./BaseScript.sol";

contract LiquidityHelpers is BaseScript {
    using CurrencyLibrary for Currency;

    function _mintLiquidityParams(
        PoolKey memory poolKey,
        int24 _tickLower,
        int24 _tickUpper,
        uint256 liquidity,
        uint256 amount0Max,
        uint256 amount1Max,
        address recipient,
        bytes memory hookData
    ) internal pure returns (bytes memory, bytes[] memory) {
        bytes memory actions = abi.encodePacked(
            uint8(Actions.MINT_POSITION), uint8(Actions.SETTLE_PAIR), uint8(Actions.SWEEP), uint8(Actions.SWEEP)
        );

        bytes[] memory params = new bytes[](4);
        params[0] = abi.encode(poolKey, _tickLower, _tickUpper, liquidity, amount0Max, amount1Max, recipient, hookData);
        params[1] = abi.encode(poolKey.currency0, poolKey.currency1);
        params[2] = abi.encode(poolKey.currency0, recipient);
        params[3] = abi.encode(poolKey.currency1, recipient);

        return (actions, params);
    }

    function tokenApprovals() public {
        if (!currency0.isAddressZero()) {
            token0.approve(address(permit2), type(uint256).max);
            permit2.approve(address(token0), address(positionManager), type(uint160).max, type(uint48).max);
        }

        if (!currency1.isAddressZero()) {
            token1.approve(address(permit2), type(uint256).max);
            permit2.approve(address(token1), address(positionManager), type(uint160).max, type(uint48).max);
        }
    }

    function truncateTickSpacing(int24 tick, int24 tickSpacing) internal pure returns (int24) {
        /// forge-lint: disable-next-line(divide-before-multiply)
        return ((tick / tickSpacing) * tickSpacing);
    }
}


================================================
FILE: script/testing/00_DeployV4.s.sol
================================================
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol";
import {HookMiner} from "@uniswap/v4-periphery/src/utils/HookMiner.sol";

import {console2} from "forge-std/Script.sol";
import {BaseScript} from "../base/BaseScript.sol";

contract DeployLocalV4 is BaseScript {
    function run() public {
        require(block.chainid == 31337, "Local deployment only");
        /**
         * Important:
         *
         * This script deploys the Uniswap V4 artifacts to local Anvil network.
         * That said, scripts in this repo will NOT automatically use these deployments,
         * unless you also change the addresses in the `Deployers.sol` file.
         *
         * You can override or modify the following functions with your own deployments:
         * - deployPoolManager()
         * - deployPositionManager()
         * - deployRouter()
         *
         * Permit2 is always on the same address.
         */

        vm.startBroadcast();
        deployArtifacts();
        vm.stopBroadcast();

        console2.log("Deployed Permit2 at:", address(permit2));
        console2.log("Deployed V4PoolManager at:", address(poolManager));
        console2.log("Deployed V4PositionManager at:", address(positionManager));
        console2.log("Deployed V4SwapRouter at:", address(swapRouter));
    }
}


================================================
FILE: src/Counter.sol
================================================
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

import {BaseHook} from "@openzeppelin/uniswap-hooks/src/base/BaseHook.sol";

import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol";
import {IPoolManager, SwapParams, ModifyLiquidityParams} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol";
import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol";
import {BeforeSwapDelta, BeforeSwapDeltaLibrary} from "@uniswap/v4-core/src/types/BeforeSwapDelta.sol";

contract Counter is BaseHook {
    using PoolIdLibrary for PoolKey;

    // NOTE: ---------------------------------------------------------
    // state variables should typically be unique to a pool
    // a single hook contract should be able to service multiple pools
    // ---------------------------------------------------------------

    mapping(PoolId => uint256 count) public beforeSwapCount;
    mapping(PoolId => uint256 count) public afterSwapCount;

    mapping(PoolId => uint256 count) public beforeAddLiquidityCount;
    mapping(PoolId => uint256 count) public beforeRemoveLiquidityCount;

    constructor(IPoolManager _poolManager) BaseHook(_poolManager) {}

    function getHookPermissions() public pure override returns (Hooks.Permissions memory) {
        return Hooks.Permissions({
            beforeInitialize: false,
            afterInitialize: false,
            beforeAddLiquidity: true,
            afterAddLiquidity: false,
            beforeRemoveLiquidity: true,
            afterRemoveLiquidity: false,
            beforeSwap: true,
            afterSwap: true,
            beforeDonate: false,
            afterDonate: false,
            beforeSwapReturnDelta: false,
            afterSwapReturnDelta: false,
            afterAddLiquidityReturnDelta: false,
            afterRemoveLiquidityReturnDelta: false
        });
    }

    // -----------------------------------------------
    // NOTE: see IHooks.sol for function documentation
    // -----------------------------------------------

    function _beforeSwap(address, PoolKey calldata key, SwapParams calldata, bytes calldata)
        internal
        override
        returns (bytes4, BeforeSwapDelta, uint24)
    {
        beforeSwapCount[key.toId()]++;
        return (BaseHook.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, 0);
    }

    function _afterSwap(address, PoolKey calldata key, SwapParams calldata, BalanceDelta, bytes calldata)
        internal
        override
        returns (bytes4, int128)
    {
        afterSwapCount[key.toId()]++;
        return (BaseHook.afterSwap.selector, 0);
    }

    function _beforeAddLiquidity(address, PoolKey calldata key, ModifyLiquidityParams calldata, bytes calldata)
        internal
        override
        returns (bytes4)
    {
        beforeAddLiquidityCount[key.toId()]++;
        return BaseHook.beforeAddLiquidity.selector;
    }

    function _beforeRemoveLiquidity(address, PoolKey calldata key, ModifyLiquidityParams calldata, bytes calldata)
        internal
        override
        returns (bytes4)
    {
        beforeRemoveLiquidityCount[key.toId()]++;
        return BaseHook.beforeRemoveLiquidity.selector;
    }
}


================================================
FILE: test/Counter.t.sol
================================================
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

import {Test} from "forge-std/Test.sol";

import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol";
import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol";
import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol";
import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol";
import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol";
import {CurrencyLibrary, Currency} from "@uniswap/v4-core/src/types/Currency.sol";
import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol";
import {LiquidityAmounts} from "@uniswap/v4-core/test/utils/LiquidityAmounts.sol";
import {IPositionManager} from "@uniswap/v4-periphery/src/interfaces/IPositionManager.sol";
import {Constants} from "@uniswap/v4-core/test/utils/Constants.sol";

import {EasyPosm} from "./utils/libraries/EasyPosm.sol";

import {Counter} from "../src/Counter.sol";
import {BaseTest} from "./utils/BaseTest.sol";

contract CounterTest is BaseTest {
    using EasyPosm for IPositionManager;
    using PoolIdLibrary for PoolKey;
    using CurrencyLibrary for Currency;
    using StateLibrary for IPoolManager;

    Currency currency0;
    Currency currency1;

    PoolKey poolKey;

    Counter hook;
    PoolId poolId;

    uint256 tokenId;
    int24 tickLower;
    int24 tickUpper;

    function setUp() public {
        // Deploys all required artifacts.
        deployArtifactsAndLabel();

        (currency0, currency1) = deployCurrencyPair();

        // Deploy the hook to an address with the correct flags
        address flags = address(
            uint160(
                Hooks.BEFORE_SWAP_FLAG | Hooks.AFTER_SWAP_FLAG | Hooks.BEFORE_ADD_LIQUIDITY_FLAG
                    | Hooks.BEFORE_REMOVE_LIQUIDITY_FLAG
            ) ^ (0x4444 << 144) // Namespace the hook to avoid collisions
        );
        bytes memory constructorArgs = abi.encode(poolManager); // Add all the necessary constructor arguments from the hook
        deployCodeTo("Counter.sol:Counter", constructorArgs, flags);
        hook = Counter(flags);

        // Create the pool
        poolKey = PoolKey(currency0, currency1, 3000, 60, IHooks(hook));
        poolId = poolKey.toId();
        poolManager.initialize(poolKey, Constants.SQRT_PRICE_1_1);

        // Provide full-range liquidity to the pool
        tickLower = TickMath.minUsableTick(poolKey.tickSpacing);
        tickUpper = TickMath.maxUsableTick(poolKey.tickSpacing);

        uint128 liquidityAmount = 100e18;

        (uint256 amount0Expected, uint256 amount1Expected) = LiquidityAmounts.getAmountsForLiquidity(
            Constants.SQRT_PRICE_1_1,
            TickMath.getSqrtPriceAtTick(tickLower),
            TickMath.getSqrtPriceAtTick(tickUpper),
            liquidityAmount
        );

        (tokenId,) = positionManager.mint(
            poolKey,
            tickLower,
            tickUpper,
            liquidityAmount,
            amount0Expected + 1,
            amount1Expected + 1,
            address(this),
            block.timestamp,
            Constants.ZERO_BYTES
        );
    }

    function testCounterHooks() public {
        // positions were created in setup()
        assertEq(hook.beforeAddLiquidityCount(poolId), 1);
        assertEq(hook.beforeRemoveLiquidityCount(poolId), 0);

        assertEq(hook.beforeSwapCount(poolId), 0);
        assertEq(hook.afterSwapCount(poolId), 0);

        // Perform a test swap //
        uint256 amountIn = 1e18;
        BalanceDelta swapDelta = swapRouter.swapExactTokensForTokens({
            amountIn: amountIn,
            amountOutMin: 0, // Very bad, but we want to allow for unlimited price impact
            zeroForOne: true,
            poolKey: poolKey,
            hookData: Constants.ZERO_BYTES,
            receiver: address(this),
            deadline: block.timestamp + 1
        });
        // ------------------- //

        assertEq(int256(swapDelta.amount0()), -int256(amountIn));

        assertEq(hook.beforeSwapCount(poolId), 1);
        assertEq(hook.afterSwapCount(poolId), 1);
    }

    function testLiquidityHooks() public {
        // positions were created in setup()
        assertEq(hook.beforeAddLiquidityCount(poolId), 1);
        assertEq(hook.beforeRemoveLiquidityCount(poolId), 0);

        // remove liquidity
        uint256 liquidityToRemove = 1e18;
        positionManager.decreaseLiquidity(
            tokenId,
            liquidityToRemove,
            0, // Max slippage, token0
            0, // Max slippage, token1
            address(this),
            block.timestamp,
            Constants.ZERO_BYTES
        );

        assertEq(hook.beforeAddLiquidityCount(poolId), 1);
        assertEq(hook.beforeRemoveLiquidityCount(poolId), 1);
    }
}


================================================
FILE: test/utils/BaseTest.sol
================================================
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.26;

import {Test} from "forge-std/Test.sol";
import {Currency} from "@uniswap/v4-core/src/types/Currency.sol";

import {Deployers} from "./Deployers.sol";

contract BaseTest is Test, Deployers {
    function deployArtifactsAndLabel() internal {
        deployArtifacts();

        vm.label(address(permit2), "Permit2");
        vm.label(address(poolManager), "V4PoolManager");
        vm.label(address(positionManager), "V4PositionManager");
        vm.label(address(swapRouter), "V4SwapRouter");
    }

    function deployCurrencyPair() internal virtual override returns (Currency currency0, Currency currency1) {
        (currency0, currency1) = super.deployCurrencyPair();

        vm.label(Currency.unwrap(currency0), "Currency0");
        vm.label(Currency.unwrap(currency1), "Currency1");
    }

    function _etch(address target, bytes memory bytecode) internal override {
        vm.etch(target, bytecode);
    }
}


================================================
FILE: test/utils/Deployers.sol
================================================
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.26;

import {MockERC20} from "solmate/src/test/utils/mocks/MockERC20.sol";

import {Currency} from "@uniswap/v4-core/src/types/Currency.sol";

import {IPermit2} from "permit2/src/interfaces/IPermit2.sol";
import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
import {IPositionManager} from "@uniswap/v4-periphery/src/interfaces/IPositionManager.sol";

import {IUniswapV4Router04} from "hookmate/interfaces/router/IUniswapV4Router04.sol";
import {AddressConstants} from "hookmate/constants/AddressConstants.sol";

import {Permit2Deployer} from "hookmate/artifacts/Permit2.sol";
import {V4PoolManagerDeployer} from "hookmate/artifacts/V4PoolManager.sol";
import {V4PositionManagerDeployer} from "hookmate/artifacts/V4PositionManager.sol";
import {V4RouterDeployer} from "hookmate/artifacts/V4Router.sol";

/**
 * Base Deployer Contract for Hook Testing
 *
 * Automatically does the following:
 * 1. Setup deployments for Permit2, PoolManager, PositionManager and V4SwapRouter.
 * 2. Check if chainId is 31337, is so, deploys local instances.
 * 3. If not, uses existing canonical deployments on the selected network.
 * 4. Provides utility functions to deploy tokens and currency pairs.
 *
 * This contract can be used for both local testing and fork testing.
 */
abstract contract Deployers {
    IPermit2 permit2;
    IPoolManager poolManager;
    IPositionManager positionManager;
    IUniswapV4Router04 swapRouter;

    function deployToken() internal returns (MockERC20 token) {
        token = new MockERC20("Test Token", "TEST", 18);
        token.mint(address(this), 10_000_000 ether);

        token.approve(address(permit2), type(uint256).max);
        token.approve(address(swapRouter), type(uint256).max);

        permit2.approve(address(token), address(positionManager), type(uint160).max, type(uint48).max);
        permit2.approve(address(token), address(poolManager), type(uint160).max, type(uint48).max);
    }

    function deployCurrencyPair() internal virtual returns (Currency currency0, Currency currency1) {
        MockERC20 token0 = deployToken();
        MockERC20 token1 = deployToken();

        if (token0 > token1) {
            (token0, token1) = (token1, token0);
        }

        currency0 = Currency.wrap(address(token0));
        currency1 = Currency.wrap(address(token1));
    }

    function deployPermit2() internal {
        address permit2Address = AddressConstants.getPermit2Address();

        if (permit2Address.code.length > 0) {
            // Permit2 is already deployed, no need to etch it.
        } else {
            _etch(permit2Address, Permit2Deployer.deploy().code);
        }

        permit2 = IPermit2(permit2Address);
    }

    function deployPoolManager() internal virtual {
        if (block.chainid == 31337) {
            poolManager = IPoolManager(V4PoolManagerDeployer.deploy(address(0x4444)));
        } else {
            poolManager = IPoolManager(AddressConstants.getPoolManagerAddress(block.chainid));
        }
    }

    function deployPositionManager() internal virtual {
        if (block.chainid == 31337) {
            positionManager = IPositionManager(
                V4PositionManagerDeployer.deploy(
                    address(poolManager), address(permit2), 300_000, address(0), address(0)
                )
            );
        } else {
            positionManager = IPositionManager(AddressConstants.getPositionManagerAddress(block.chainid));
        }
    }

    function deployRouter() internal virtual {
        if (block.chainid == 31337) {
            swapRouter = IUniswapV4Router04(payable(V4RouterDeployer.deploy(address(poolManager), address(permit2))));
        } else {
            swapRouter = IUniswapV4Router04(payable(AddressConstants.getV4SwapRouterAddress(block.chainid)));
        }
    }

    function _etch(address, bytes memory) internal virtual {
        revert("Not implemented");
    }

    function deployArtifacts() internal {
        // Order matters.
        deployPermit2();
        deployPoolManager();
        deployPositionManager();
        deployRouter();
    }
}


================================================
FILE: test/utils/libraries/EasyPosm.sol
================================================
// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity ^0.8.21;

import {PoolKey} from "v4-core/src/types/PoolKey.sol";
import {BalanceDelta, toBalanceDelta} from "v4-core/src/types/BalanceDelta.sol";
import {Currency, CurrencyLibrary} from "v4-core/src/types/Currency.sol";
import {IPositionManager} from "@uniswap/v4-periphery/src/interfaces/IPositionManager.sol";
import {Actions} from "@uniswap/v4-periphery/src/libraries/Actions.sol";
import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol";
import {PositionInfo, PositionInfoLibrary} from "@uniswap/v4-periphery/src/libraries/PositionInfoLibrary.sol";

/// @title Easy Position Manager
/// @notice A library for abstracting Position Manager calldata
/// @dev Useable onchain, but expensive because of encoding
library EasyPosm {
    using CurrencyLibrary for Currency;
    using SafeCast for uint256;
    using SafeCast for int256;
    using PositionInfoLibrary for PositionInfo;

    /// @dev packing data to avoid stack too deep error
    struct MintData {
        uint256 balance0Before;
        uint256 balance1Before;
        bytes[] params;
        bytes actions;
    }

    /// @dev This function supports sending native tokens (ETH), the amount-to-pay is determined by amount0Max.
    ///      Any excess amount is NOT refunded since it is not encoding the SWEEP action
    function mint(
        IPositionManager posm,
        PoolKey memory poolKey,
        int24 tickLower,
        int24 tickUpper,
        uint256 liquidity,
        uint256 amount0Max,
        uint256 amount1Max,
        address recipient,
        uint256 deadline,
        bytes memory hookData
    ) internal returns (uint256 tokenId, BalanceDelta delta) {
        (Currency currency0, Currency currency1) = (poolKey.currency0, poolKey.currency1);

        MintData memory mintData = MintData({
            balance0Before: currency0.balanceOf(address(this)),
            balance1Before: currency1.balanceOf(address(this)),
            actions: new bytes(0),
            params: new bytes[](4)
        });

        mintData.actions = abi.encodePacked(
            uint8(Actions.MINT_POSITION), uint8(Actions.SETTLE_PAIR), uint8(Actions.SWEEP), uint8(Actions.SWEEP)
        );

        mintData.params[0] =
            abi.encode(poolKey, tickLower, tickUpper, liquidity, amount0Max, amount1Max, recipient, hookData);
        mintData.params[1] = abi.encode(currency0, currency1);
        mintData.params[2] = abi.encode(currency0, recipient);
        mintData.params[3] = abi.encode(currency1, recipient);

        // Mint Liquidity
        tokenId = posm.nextTokenId();
        uint256 valueToPass = currency0.isAddressZero() ? amount0Max : 0;
        posm.modifyLiquidities{value: valueToPass}(abi.encode(mintData.actions, mintData.params), deadline);

        delta = toBalanceDelta(
            -(mintData.balance0Before - currency0.balanceOf(address(this))).toInt128(),
            -(mintData.balance1Before - currency1.balanceOf(address(this))).toInt128()
        );
    }

    function increaseLiquidity(
        IPositionManager posm,
        uint256 tokenId,
        uint256 liquidityToAdd,
        uint256 amount0Max,
        uint256 amount1Max,
        uint256 deadline,
        bytes memory hookData
    ) internal returns (BalanceDelta delta) {
        (Currency currency0, Currency currency1) = getCurrencies(posm, tokenId);

        bytes[] memory params = new bytes[](3);
        params[0] = abi.encode(tokenId, liquidityToAdd, amount0Max, amount1Max, hookData);
        params[1] = abi.encode(currency0);
        params[2] = abi.encode(currency1);

        uint256 balance0Before = currency0.balanceOf(address(this));
        uint256 balance1Before = currency1.balanceOf(address(this));

        uint256 valueToPass = currency0.isAddressZero() ? amount0Max : 0;
        posm.modifyLiquidities{value: valueToPass}(
            abi.encode(
                abi.encodePacked(
                    uint8(Actions.INCREASE_LIQUIDITY), uint8(Actions.CLOSE_CURRENCY), uint8(Actions.CLOSE_CURRENCY)
                ),
                params
            ),
            deadline
        );

        delta = toBalanceDelta(
            (currency0.balanceOf(address(this)).toInt256() - balance0Before.toInt256()).toInt128(),
            (currency1.balanceOf(address(this)).toInt256() - balance1Before.toInt256()).toInt128()
        );
    }

    function decreaseLiquidity(
        IPositionManager posm,
        uint256 tokenId,
        uint256 liquidityToRemove,
        uint256 amount0Min,
        uint256 amount1Min,
        address recipient,
        uint256 deadline,
        bytes memory hookData
    ) internal returns (BalanceDelta delta) {
        (Currency currency0, Currency currency1) = getCurrencies(posm, tokenId);

        bytes[] memory params = new bytes[](2);
        params[0] = abi.encode(tokenId, liquidityToRemove, amount0Min, amount1Min, hookData);
        params[1] = abi.encode(currency0, currency1, recipient);

        uint256 balance0Before = currency0.balanceOf(address(this));
        uint256 balance1Before = currency1.balanceOf(address(this));

        posm.modifyLiquidities(
            abi.encode(abi.encodePacked(uint8(Actions.DECREASE_LIQUIDITY), uint8(Actions.TAKE_PAIR)), params), deadline
        );

        delta = toBalanceDelta(
            (currency0.balanceOf(address(this)) - balance0Before).toInt128(),
            (currency1.balanceOf(address(this)) - balance1Before).toInt128()
        );
    }

    function collect(
        IPositionManager posm,
        uint256 tokenId,
        uint256 amount0Min,
        uint256 amount1Min,
        address recipient,
        uint256 deadline,
        bytes memory hookData
    ) internal returns (BalanceDelta delta) {
        (Currency currency0, Currency currency1) = getCurrencies(posm, tokenId);

        bytes[] memory params = new bytes[](2);
        // collecting fees is achieved by decreasing liquidity with 0 liquidity removed
        params[0] = abi.encode(tokenId, 0, amount0Min, amount1Min, hookData);
        params[1] = abi.encode(currency0, currency1, recipient);

        uint256 balance0Before = currency0.balanceOf(recipient);
        uint256 balance1Before = currency1.balanceOf(recipient);

        posm.modifyLiquidities(
            abi.encode(abi.encodePacked(uint8(Actions.DECREASE_LIQUIDITY), uint8(Actions.TAKE_PAIR)), params), deadline
        );

        delta = toBalanceDelta(
            (currency0.balanceOf(recipient) - balance0Before).toInt128(),
            (currency1.balanceOf(recipient) - balance1Before).toInt128()
        );
    }

    function burn(
        IPositionManager posm,
        uint256 tokenId,
        uint256 amount0Min,
        uint256 amount1Min,
        address recipient,
        uint256 deadline,
        bytes memory hookData
    ) internal returns (BalanceDelta delta) {
        (Currency currency0, Currency currency1) = getCurrencies(posm, tokenId);

        bytes[] memory params = new bytes[](2);
        params[0] = abi.encode(tokenId, 0, amount0Min, amount1Min, hookData);
        params[1] = abi.encode(currency0, currency1, recipient);

        uint256 balance0Before = currency0.balanceOf(recipient);
        uint256 balance1Before = currency1.balanceOf(recipient);

        posm.modifyLiquidities(
            abi.encode(abi.encodePacked(uint8(Actions.BURN_POSITION), uint8(Actions.TAKE_PAIR)), params), deadline
        );

        delta = toBalanceDelta(
            (currency0.balanceOf(recipient) - balance0Before).toInt128(),
            (currency1.balanceOf(recipient) - balance1Before).toInt128()
        );
    }

    function getCurrencies(IPositionManager posm, uint256 tokenId)
        internal
        view
        returns (Currency currency0, Currency currency1)
    {
        (PoolKey memory key,) = posm.getPoolAndPositionInfo(tokenId);
        return (key.currency0, key.currency1);
    }
}


================================================
FILE: test/utils/libraries/EasyPosm.t.sol
================================================
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

import {Test} from "forge-std/Test.sol";
import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol";
import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol";
import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol";
import {PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol";
import {CurrencyLibrary, Currency} from "@uniswap/v4-core/src/types/Currency.sol";
import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol";
import {LiquidityAmounts} from "@uniswap/v4-core/test/utils/LiquidityAmounts.sol";
import {IPositionManager} from "@uniswap/v4-periphery/src/interfaces/IPositionManager.sol";
import {Constants} from "@uniswap/v4-core/test/utils/Constants.sol";

import {EasyPosm} from "./EasyPosm.sol";
import {BaseTest} from "../BaseTest.sol";

contract EasyPosmTest is Test, BaseTest {
    using EasyPosm for IPositionManager;
    using PoolIdLibrary for PoolKey;
    using CurrencyLibrary for Currency;
    using StateLibrary for IPoolManager;

    Currency currency0;
    Currency currency1;

    int24 tickLower;
    int24 tickUpper;

    PoolKey key;
    PoolKey nativeKey;

    function setUp() public {
        deployArtifacts();

        (currency0, currency1) = deployCurrencyPair();

        // Create the pool
        key = PoolKey(currency0, currency1, 3000, 60, IHooks(address(0)));
        nativeKey = PoolKey(Currency.wrap(address(0)), currency1, 3000, 60, IHooks(address(0)));

        poolManager.initialize(key, Constants.SQRT_PRICE_1_1);
        poolManager.initialize(nativeKey, Constants.SQRT_PRICE_1_1);

        // full-range liquidity
        tickLower = TickMath.minUsableTick(key.tickSpacing);
        tickUpper = TickMath.maxUsableTick(key.tickSpacing);
    }

    function test_mintLiquidity() public {
        uint256 liquidityToMint = 100e18;
        address recipient = address(this);

        (uint256 amount0, uint256 amount1) = LiquidityAmounts.getAmountsForLiquidity(
            Constants.SQRT_PRICE_1_1,
            TickMath.getSqrtPriceAtTick(tickLower),
            TickMath.getSqrtPriceAtTick(tickUpper),
            uint128(liquidityToMint)
        );

        (, BalanceDelta delta) = positionManager.mint(
            key,
            tickLower,
            tickUpper,
            liquidityToMint,
            type(uint256).max,
            type(uint256).max,
            recipient,
            block.timestamp + 1,
            Constants.ZERO_BYTES
        );
        assertEq(delta.amount0(), -int128(uint128(amount0 + 1 wei)));
        assertEq(delta.amount1(), -int128(uint128(amount1 + 1 wei)));
    }

    function test_mintLiquidityNative() public {
        uint256 liquidityToMint = 100e18;
        address recipient = address(this);

        (uint256 amount0, uint256 amount1) = LiquidityAmounts.getAmountsForLiquidity(
            Constants.SQRT_PRICE_1_1,
            TickMath.getSqrtPriceAtTick(tickLower),
            TickMath.getSqrtPriceAtTick(tickUpper),
            uint128(liquidityToMint)
        );

        vm.deal(address(this), amount0 + 1);
        (, BalanceDelta delta) = positionManager.mint(
            nativeKey,
            tickLower,
            tickUpper,
            liquidityToMint,
            amount0 + 1,
            amount1 + 1,
            recipient,
            block.timestamp + 1,
            Constants.ZERO_BYTES
        );
        assertEq(delta.amount0(), -int128(uint128(amount0 + 1 wei)));
        assertEq(delta.amount1(), -int128(uint128(amount1 + 1 wei)));
    }

    function test_increaseLiquidity() public {
        (uint256 tokenId,) = positionManager.mint(
            key,
            tickLower,
            tickUpper,
            100e18,
            type(uint256).max,
            type(uint256).max,
            address(this),
            block.timestamp + 1,
            Constants.ZERO_BYTES
        );

        uint256 liquidityToAdd = 1e18;

        (uint256 amount0, uint256 amount1) = LiquidityAmounts.getAmountsForLiquidity(
            Constants.SQRT_PRICE_1_1,
            TickMath.getSqrtPriceAtTick(tickLower),
            TickMath.getSqrtPriceAtTick(tickUpper),
            uint128(liquidityToAdd)
        );

        BalanceDelta delta = positionManager.increaseLiquidity(
            tokenId, liquidityToAdd, type(uint256).max, type(uint256).max, block.timestamp + 1, Constants.ZERO_BYTES
        );
        assertEq(delta.amount0(), -int128(uint128(amount0 + 1 wei)));
        assertEq(delta.amount1(), -int128(uint128(amount1 + 1 wei)));
    }

    function test_increaseLiquidityNative() public {
        uint256 liquidityToMint = 100e18;
        address recipient = address(this);

        (uint256 amount0, uint256 amount1) = LiquidityAmounts.getAmountsForLiquidity(
            Constants.SQRT_PRICE_1_1,
            TickMath.getSqrtPriceAtTick(tickLower),
            TickMath.getSqrtPriceAtTick(tickUpper),
            uint128(liquidityToMint)
        );

        vm.deal(address(this), amount0 + 1);
        (uint256 tokenId, BalanceDelta delta) = positionManager.mint(
            nativeKey,
            tickLower,
            tickUpper,
            liquidityToMint,
            amount0 + 1,
            amount1 + 1,
            recipient,
            block.timestamp + 1,
            Constants.ZERO_BYTES
        );

        uint256 liquidityToIncrease = 1e18;

        (amount0, amount1) = LiquidityAmounts.getAmountsForLiquidity(
            Constants.SQRT_PRICE_1_1,
            TickMath.getSqrtPriceAtTick(tickLower),
            TickMath.getSqrtPriceAtTick(tickUpper),
            uint128(liquidityToIncrease)
        );

        vm.deal(address(this), amount0 + 1);
        delta = positionManager.increaseLiquidity(
            tokenId, liquidityToIncrease, amount0 + 1, amount1 + 1, block.timestamp + 1, Constants.ZERO_BYTES
        );
        assertEq(delta.amount0(), -int128(uint128(amount0 + 1 wei)));
        assertEq(delta.amount1(), -int128(uint128(amount1 + 1 wei)));
    }

    function test_decreaseLiquidity() public {
        (uint256 tokenId,) = positionManager.mint(
            key,
            tickLower,
            tickUpper,
            100e18,
            type(uint256).max,
            type(uint256).max,
            address(this),
            block.timestamp + 1,
            Constants.ZERO_BYTES
        );

        uint256 liquidityToRemove = 1e18;

        (uint256 amount0, uint256 amount1) = LiquidityAmounts.getAmountsForLiquidity(
            Constants.SQRT_PRICE_1_1,
            TickMath.getSqrtPriceAtTick(tickLower),
            TickMath.getSqrtPriceAtTick(tickUpper),
            uint128(liquidityToRemove)
        );

        BalanceDelta delta = positionManager.decreaseLiquidity(
            tokenId, liquidityToRemove, 0, 0, address(this), block.timestamp + 1, Constants.ZERO_BYTES
        );
        assertEq(delta.amount0(), int128(uint128(amount0)));
        assertEq(delta.amount1(), int128(uint128(amount1)));
    }

    function test_burn() public {
        (uint256 tokenId, BalanceDelta mintDelta) = positionManager.mint(
            key,
            tickLower,
            tickUpper,
            100e18,
            type(uint256).max,
            type(uint256).max,
            address(this),
            block.timestamp + 1,
            Constants.ZERO_BYTES
        );

        BalanceDelta delta =
            positionManager.burn(tokenId, 0, 0, address(this), block.timestamp + 1, Constants.ZERO_BYTES);
        assertEq(delta.amount0(), -mintDelta.amount0() - 1 wei);
        assertEq(delta.amount1(), -mintDelta.amount1() - 1 wei);
    }

    // This test requires a donateRouter, TODO
    // function test_collect() public {
    //     (uint256 tokenId,) = positionManager.mint(
    //         key,
    //         tickLower,
    //         tickUpper,
    //         100e18,
    //         type(uint256).max,
    //         type(uint256).max,
    //         address(this),
    //         block.timestamp + 1,
    //         Constants.ZERO_BYTES
    //     );

    //     // donate to regenerate fee revenue
    //     uint128 feeRevenue0 = 1e18;
    //     uint128 feeRevenue1 = 0.1e18;

    //     poolManager.donate(key, feeRevenue0, feeRevenue1, Constants.ZERO_BYTES);

    //     // position collects half of the revenue since 50% of the liquidity is minted in setUp()
    //     BalanceDelta delta =
    //         positionManager.collect(tokenId, 0, 0, address(0x123), block.timestamp + 1, Constants.ZERO_BYTES);
    //     assertEq(uint128(delta.amount0()), feeRevenue0 - 1 wei);
    //     assertEq(uint128(delta.amount1()), feeRevenue1 - 1 wei);
    // }
}
Download .txt
gitextract_c36u9obz/

├── .github/
│   └── workflows/
│       └── test.yml
├── .gitignore
├── .gitmodules
├── .vscode/
│   └── settings.json
├── LICENSE
├── README.md
├── foundry.toml
├── remappings.txt
├── script/
│   ├── 00_DeployHook.s.sol
│   ├── 01_CreatePoolAndAddLiquidity.s.sol
│   ├── 02_AddLiquidity.s.sol
│   ├── 03_Swap.s.sol
│   ├── base/
│   │   ├── BaseScript.sol
│   │   └── LiquidityHelpers.sol
│   └── testing/
│       └── 00_DeployV4.s.sol
├── src/
│   └── Counter.sol
└── test/
    ├── Counter.t.sol
    └── utils/
        ├── BaseTest.sol
        ├── Deployers.sol
        └── libraries/
            ├── EasyPosm.sol
            └── EasyPosm.t.sol
Condensed preview — 21 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (57K chars).
[
  {
    "path": ".github/workflows/test.yml",
    "chars": 563,
    "preview": "name: Test Suite\n\non:\n  workflow_dispatch:\n  # push:\n  # pull_request:\n\nenv:\n  FOUNDRY_PROFILE: ci\n\njobs:\n  test:\n    na"
  },
  {
    "path": ".gitignore",
    "chars": 214,
    "preview": "# Compiler files\ncache/\nout/\n\n# Ignores development (and goerli) broadcast logs\n!/broadcast\n/broadcast/*/31337/\n/broadca"
  },
  {
    "path": ".gitmodules",
    "chars": 303,
    "preview": "[submodule \"lib/forge-std\"]\n\tpath = lib/forge-std\n\turl = https://github.com/foundry-rs/forge-std\n[submodule \"lib/uniswap"
  },
  {
    "path": ".vscode/settings.json",
    "chars": 36,
    "preview": "{\n  \"solidity.formatter\": \"forge\"\n}\n"
  },
  {
    "path": "LICENSE",
    "chars": 1067,
    "preview": "MIT License\n\nCopyright (c) 2023 saucepoint\n\nPermission is hereby granted, free of charge, to any person obtaining a copy"
  },
  {
    "path": "README.md",
    "chars": 6308,
    "preview": "# Uniswap v4 Hook Template\n\n**A template for writing Uniswap v4 Hooks 🦄**\n\n### Get Started\n\nThis template provides a sta"
  },
  {
    "path": "foundry.toml",
    "chars": 335,
    "preview": "[profile.default]\nbytecode_hash = \"none\"\nevm_version = \"cancun\"\nffi = true\nfs_permissions = [{access = \"read-write\", pat"
  },
  {
    "path": "remappings.txt",
    "chars": 508,
    "preview": "forge-std/=lib/forge-std/src/\n\n@uniswap/v4-core/=lib/uniswap-hooks/lib/v4-core/\n@uniswap/v4-periphery/=lib/uniswap-hooks"
  },
  {
    "path": "script/00_DeployHook.s.sol",
    "chars": 1250,
    "preview": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.26;\n\nimport {Hooks} from \"@uniswap/v4-core/src/libraries/Hooks.sol\""
  },
  {
    "path": "script/01_CreatePoolAndAddLiquidity.s.sol",
    "chars": 3117,
    "preview": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.26;\n\nimport {PoolKey} from \"@uniswap/v4-core/src/types/PoolKey.sol\""
  },
  {
    "path": "script/02_AddLiquidity.s.sol",
    "chars": 3055,
    "preview": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.26;\n\nimport {IPoolManager} from \"@uniswap/v4-core/src/interfaces/IP"
  },
  {
    "path": "script/03_Swap.s.sol",
    "chars": 1139,
    "preview": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.26;\n\nimport {PoolKey} from \"@uniswap/v4-core/src/types/PoolKey.sol\""
  },
  {
    "path": "script/base/BaseScript.sol",
    "chars": 2864,
    "preview": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.26;\n\nimport {Script} from \"forge-std/Script.sol\";\nimport {IERC20} f"
  },
  {
    "path": "script/base/LiquidityHelpers.sol",
    "chars": 2029,
    "preview": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.20;\n\nimport {PoolKey} from \"@uniswap/v4-core/src/types/PoolKey.sol\""
  },
  {
    "path": "script/testing/00_DeployV4.s.sol",
    "chars": 1376,
    "preview": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.26;\n\nimport {Hooks} from \"@uniswap/v4-core/src/libraries/Hooks.sol\""
  },
  {
    "path": "src/Counter.sol",
    "chars": 3325,
    "preview": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.26;\n\nimport {BaseHook} from \"@openzeppelin/uniswap-hooks/src/base/B"
  },
  {
    "path": "test/Counter.t.sol",
    "chars": 4948,
    "preview": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\n\nimport {IHooks} from"
  },
  {
    "path": "test/utils/BaseTest.sol",
    "chars": 984,
    "preview": "// SPDX-License-Identifier: UNLICENSED\npragma solidity ^0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {Curren"
  },
  {
    "path": "test/utils/Deployers.sol",
    "chars": 4171,
    "preview": "// SPDX-License-Identifier: UNLICENSED\npragma solidity ^0.8.26;\n\nimport {MockERC20} from \"solmate/src/test/utils/mocks/M"
  },
  {
    "path": "test/utils/libraries/EasyPosm.sol",
    "chars": 7929,
    "preview": "// SPDX-License-Identifier: GPL-2.0-or-later\npragma solidity ^0.8.21;\n\nimport {PoolKey} from \"v4-core/src/types/PoolKey."
  },
  {
    "path": "test/utils/libraries/EasyPosm.t.sol",
    "chars": 8816,
    "preview": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {IHooks} from "
  }
]

About this extraction

This page contains the full source code of the uniswapfoundation/v4-template GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 21 files (53.1 KB), approximately 13.3k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!