V4PoolLiquiditySeeder

0xb5a2ffd350c58e560ddeab5d6e5aa6bff500177a

Verification
Verified
v0.8.28+commit.7893614a
Type
Contract
5,587 bytes
ABI entries
26
3 read · 9 write
License
none

Contract information

Address
0xb5a2ffd350c58e560ddeab5d6e5aa6bff500177a
Chain
Robinhood Chain (4663)
Compiler
v0.8.28+commit.7893614a
Optimization
Enabled
Creation tx
0x17ac7d6fe1…8e83f8deda

Token

Not a token

This contract does not expose ERC-20 metadata.

Read contract (3)

lpTokenBurned()bool
manager()address
owner()address

Events (5)

ERC20RescuedLpTokenBurnedNativeEthRescuedOwnershipTransferredPoolSeeded

ABI

[
  {
    "inputs": [
      {
        "internalType": "contract IPoolManager",
        "name": "poolManager",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "initialOwner",
        "type": "address"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "inputs": [],
    "name": "LpTokenAlreadyBurned",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NotPoolManager",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "owner",
        "type": "address"
      }
    ],
    "name": "OwnableInvalidOwner",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "account",
        "type": "address"
      }
    ],
    "name": "OwnableUnauthorizedAccount",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "RefundFailed",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "SafeERC20FailedOperation",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ZeroAddress",
    "type": "error"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "ERC20Rescued",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "owner",
        "type": "address"
      }
    ],
    "name": "LpTokenBurned",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "NativeEthRescued",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "previousOwner",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "OwnershipTransferred",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "payer",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "hook",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "int24",
        "name": "tickLower",
        "type": "int24"
      },
      {
        "indexed": false,
        "internalType": "int24",
        "name": "tickUpper",
        "type": "int24"
      },
      {
        "indexed": false,
        "internalType": "int256",
        "name": "liquidityDelta",
        "type": "int256"
      },
      {
        "indexed": false,
        "internalType": "int128",
        "name": "amount0Delta",
        "type": "int128"
      },
      {
        "indexed": false,
        "internalType": "int128",
        "name": "amount1Delta",
        "type": "int128"
      }
    ],
    "name": "PoolSeeded",
    "type": "event"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "pensionToken",
        "type": "address"
      },
      {
        "internalType": "contract IHooks",
        "name": "hook",
        "type": "address"
      },
      {
        "internalType": "uint24",
        "name": "fee",
        "type": "uint24"
      },
      {
        "internalType": "int24",
        "name": "tickSpacing",
        "type": "int24"
      },
      {
        "components": [
          {
            "internalType": "int24",
            "name": "tickLower",
            "type": "int24"
          },
          {
            "internalType": "int24",
            "name": "tickUpper",
            "type": "int24"
          },
          {
            "internalType": "int256",
            "name": "liquidityDelta",
            "type": "int256"
          },
          {
            "internalType": "bytes32",
            "name": "salt",
            "type": "bytes32"
          }
        ],
        "internalType": "struct IPoolManager.ModifyLiquidityParams",
        "name": "params",
        "type": "tuple"
      }
    ],
    "name": "addLiquidity",
    "outputs": [
      {
        "internalType": "BalanceDelta",
        "name": "delta",
        "type": "int256"
      }
    ],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "burnLpToken",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "pensionToken",
        "type": "address"
      },
      {
        "internalType": "contract IHooks",
        "name": "hook",
        "type": "address"
      },
      {
        "internalType": "uint24",
        "name": "fee",
        "type": "uint24"
      },
      {
        "internalType": "int24",
        "name": "tickSpacing",
        "type": "int24"
      },
      {
        "internalType": "uint160",
        "name": "sqrtPriceX96",
        "type": "uint160"
      },
      {
        "components": [
          {
            "internalType": "int24",
            "name": "tickLower",
            "type": "int24"
          },
          {
            "internalType": "int24",
            "name": "tickUpper",
            "type": "int24"
          },
          {
            "internalType": "int256",
            "name": "liquidityDelta",
            "type": "int256"
          },
          {
            "internalType": "bytes32",
            "name": "salt",
            "type": "bytes32"
          }
        ],
        "internalType": "struct IPoolManager.ModifyLiquidityParams",
        "name": "params",
        "type": "tuple"
      }
    ],
    "name": "initializeAndAddLiquidity",
    "outputs": [
      {
        "internalType": "BalanceDelta",
        "name": "delta",
        "type": "int256"
      }
    ],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "lpTokenBurned",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "manager",
    "outputs": [
      {
        "internalType": "contract IPoolManager",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "owner",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "pensionToken",
        "type": "address"
      },
      {
        "internalType": "contract IHooks",
        "name": "hook",
        "type": "address"
      },
      {
        "internalType": "uint24",
        "name": "fee",
        "type": "uint24"
      },
      {
        "internalType": "int24",
        "name": "tickSpacing",
        "type": "int24"
      },
      {
        "components": [
          {
            "internalType": "int24",
            "name": "tickLower",
            "type": "int24"
          },
          {
            "internalType": "int24",
            "name": "tickUpper",
            "type": "int24"
          },
          {
            "internalType": "int256",
            "name": "liquidityDelta",
            "type": "int256"
          },
          {
            "internalType": "bytes32",
            "name": "salt",
            "type": "bytes32"
          }
        ],
        "internalType": "struct IPoolManager.ModifyLiquidityParams",
        "name": "params",
        "type": "tuple"
      }
    ],
    "name": "removeLiquidity",
    "outputs": [
      {
        "internalType": "BalanceDelta",
        "name": "delta",
        "type": "int256"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "renounceOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "contract IERC20",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "rescueERC20",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address payable",
        "name": "to",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "rescueNative",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "transferOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes",
        "name": "rawData",
        "type": "bytes"
      }
    ],
    "name": "unlockCallback",
    "outputs": [
      {
        "internalType": "bytes",
        "name": "",
        "type": "bytes"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "stateMutability": "payable",
    "type": "receive"
  }
]

Source code

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import {IERC20} from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol";
import {Ownable} from "openzeppelin-contracts/contracts/access/Ownable.sol";

import {IUnlockCallback} from "v4-core/interfaces/callback/IUnlockCallback.sol";
import {IHooks} from "v4-core/interfaces/IHooks.sol";
import {IPoolManager} from "v4-core/interfaces/IPoolManager.sol";
import {BalanceDelta} from "v4-core/types/BalanceDelta.sol";
import {Currency, CurrencyLibrary} from "v4-core/types/Currency.sol";
import {PoolKey} from "v4-core/types/PoolKey.sol";

contract V4PoolLiquiditySeeder is IUnlockCallback, Ownable {
    using CurrencyLibrary for Currency;
    using SafeERC20 for IERC20;

    IPoolManager public immutable manager;
    bool public lpTokenBurned;

    struct CallbackData {
        address payer;
        PoolKey key;
        IPoolManager.ModifyLiquidityParams params;
    }

    event PoolSeeded(
        address indexed payer,
        address indexed token,
        address indexed hook,
        int24 tickLower,
        int24 tickUpper,
        int256 liquidityDelta,
        int128 amount0Delta,
        int128 amount1Delta
    );
    event LpTokenBurned(address indexed owner);
    event NativeEthRescued(address indexed to, uint256 amount);
    event ERC20Rescued(address indexed token, address indexed to, uint256 amount);

    error LpTokenAlreadyBurned();
    error NotPoolManager();
    error RefundFailed();
    error ZeroAddress();

    constructor(IPoolManager poolManager, address initialOwner) Ownable(initialOwner) {
        manager = poolManager;
    }

    receive() external payable {}

    function initializeAndAddLiquidity(
        address pensionToken,
        IHooks hook,
        uint24 fee,
        int24 tickSpacing,
        uint160 sqrtPriceX96,
        IPoolManager.ModifyLiquidityParams calldata params
    ) external payable onlyOwner returns (BalanceDelta delta) {
        PoolKey memory key = PoolKey({
            currency0: CurrencyLibrary.ADDRESS_ZERO,
            currency1: Currency.wrap(pensionToken),
            fee: fee,
            tickSpacing: tickSpacing,
            hooks: hook
        });

        manager.initialize(key, sqrtPriceX96);
        delta = _modifyLiquidity(msg.sender, key, params);

        _refundNative(msg.sender);

        emit PoolSeeded(
            msg.sender,
            pensionToken,
            address(hook),
            params.tickLower,
            params.tickUpper,
            params.liquidityDelta,
            delta.amount0(),
            delta.amount1()
        );
    }

    function addLiquidity(
        address pensionToken,
        IHooks hook,
        uint24 fee,
        int24 tickSpacing,
        IPoolManager.ModifyLiquidityParams calldata params
    ) external payable onlyOwner returns (BalanceDelta delta) {
        PoolKey memory key = PoolKey({
            currency0: CurrencyLibrary.ADDRESS_ZERO,
            currency1: Currency.wrap(pensionToken),
            fee: fee,
            tickSpacing: tickSpacing,
            hooks: hook
        });

        delta = _modifyLiquidity(msg.sender, key, params);
        _refundNative(msg.sender);

        emit PoolSeeded(
            msg.sender,
            pensionToken,
            address(hook),
            params.tickLower,
            params.tickUpper,
            params.liquidityDelta,
            delta.amount0(),
            delta.amount1()
        );
    }

    function removeLiquidity(
        address pensionToken,
        IHooks hook,
        uint24 fee,
        int24 tickSpacing,
        IPoolManager.ModifyLiquidityParams calldata params
    ) external onlyOwner returns (BalanceDelta delta) {
        if (lpTokenBurned) revert LpTokenAlreadyBurned();

        PoolKey memory key = PoolKey({
            currency0: CurrencyLibrary.ADDRESS_ZERO,
            currency1: Currency.wrap(pensionToken),
            fee: fee,
            tickSpacing: tickSpacing,
            hooks: hook
        });

        delta = _modifyLiquidity(msg.sender, key, params);
        _refundNative(msg.sender);

        emit PoolSeeded(
            msg.sender,
            pensionToken,
            address(hook),
            params.tickLower,
            params.tickUpper,
            params.liquidityDelta,
            delta.amount0(),
            delta.amount1()
        );
    }

    /// @notice Permanently disables liquidity removal for the position owned by this helper.
    /// @dev Uniswap v4 direct PoolManager positions do not mint ERC-20 LP tokens. This burns the helper's removal authority.
    function burnLpToken() external onlyOwner {
        if (lpTokenBurned) revert LpTokenAlreadyBurned();
        lpTokenBurned = true;
        emit LpTokenBurned(msg.sender);
    }

    function rescueNative(address payable to, uint256 amount) external onlyOwner {
        if (to == address(0)) revert ZeroAddress();
        (bool success,) = to.call{value: amount}("");
        if (!success) revert RefundFailed();
        emit NativeEthRescued(to, amount);
    }

    function rescueERC20(IERC20 token, address to, uint256 amount) external onlyOwner {
        if (to == address(0)) revert ZeroAddress();
        token.safeTransfer(to, amount);
        emit ERC20Rescued(address(token), to, amount);
    }

    function _modifyLiquidity(address payer, PoolKey memory key, IPoolManager.ModifyLiquidityParams memory params)
        internal
        returns (BalanceDelta delta)
    {
        return abi.decode(manager.unlock(abi.encode(CallbackData(payer, key, params))), (BalanceDelta));
    }

    function _refundNative(address to) internal {
        uint256 refund = address(this).balance;
        if (refund > 0) {
            (bool success,) = payable(to).call{value: refund}("");
            if (!success) revert RefundFailed();
        }
    }

    function unlockCallback(bytes calldata rawData) external returns (bytes memory) {
        if (msg.sender != address(manager)) revert NotPoolManager();

        CallbackData memory data = abi.decode(rawData, (CallbackData));
        (BalanceDelta delta,) = manager.modifyLiquidity(data.key, data.params, "");

        int128 amount0 = delta.amount0();
        int128 amount1 = delta.amount1();

        if (amount0 < 0) {
            manager.settle{value: uint128(-amount0)}();
        }

        if (amount1 < 0) {
            uint256 tokenAmount = uint128(-amount1);
            manager.sync(data.key.currency1);
            IERC20(Currency.unwrap(data.key.currency1)).safeTransferFrom(data.payer, address(manager), tokenAmount);
            manager.settle();
        }

        if (amount0 > 0) manager.take(data.key.currency0, data.payer, uint128(amount0));
        if (amount1 > 0) manager.take(data.key.currency1, data.payer, uint128(amount1));

        return abi.encode(delta);
    }
}
Chain explorer6394msChain node91ms