V4PensionSwapRouter

0x0a314bb788bedd98d4dbdffe9adf5878097a4e40

Verification
Verified
v0.8.28+commit.7893614a
Type
Contract
5,548 bytes
ABI entries
21
8 read · 3 write
License
none

Contract information

Address
0x0a314bb788bedd98d4dbdffe9adf5878097a4e40
Chain
Robinhood Chain (4663)
Compiler
v0.8.28+commit.7893614a
Optimization
Enabled
Creation tx
0x6ef1be67ae…df79ae4709

Token

Not a token

This contract does not expose ERC-20 metadata.

Read contract (8)

BIPS()uint256
TAX_BIPS()uint256
fee()uint24
hook()address
manager()address
pension()address
poolKey()tuple
tickSpacing()int24

Events (1)

RouterSwap

ABI

[
  {
    "inputs": [
      {
        "internalType": "contract IPoolManager",
        "name": "poolManager",
        "type": "address"
      },
      {
        "internalType": "contract IERC20",
        "name": "pensionToken",
        "type": "address"
      },
      {
        "internalType": "contract IHooks",
        "name": "pensionHook",
        "type": "address"
      },
      {
        "internalType": "uint24",
        "name": "poolFee",
        "type": "uint24"
      },
      {
        "internalType": "int24",
        "name": "poolTickSpacing",
        "type": "int24"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "inputs": [],
    "name": "InsufficientOutput",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidAmount",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidPool",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NotPoolManager",
    "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": "payer",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "recipient",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "bool",
        "name": "buy",
        "type": "bool"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amountIn",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amountOut",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "taxEth",
        "type": "uint256"
      }
    ],
    "name": "RouterSwap",
    "type": "event"
  },
  {
    "inputs": [],
    "name": "BIPS",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "TAX_BIPS",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "components": [
          {
            "internalType": "Currency",
            "name": "currency0",
            "type": "address"
          },
          {
            "internalType": "Currency",
            "name": "currency1",
            "type": "address"
          },
          {
            "internalType": "uint24",
            "name": "fee",
            "type": "uint24"
          },
          {
            "internalType": "int24",
            "name": "tickSpacing",
            "type": "int24"
          },
          {
            "internalType": "contract IHooks",
            "name": "hooks",
            "type": "address"
          }
        ],
        "internalType": "struct PoolKey",
        "name": "key",
        "type": "tuple"
      },
      {
        "internalType": "uint256",
        "name": "minPensionOut",
        "type": "uint256"
      },
      {
        "internalType": "address",
        "name": "recipient",
        "type": "address"
      }
    ],
    "name": "buy",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "pensionOut",
        "type": "uint256"
      }
    ],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "fee",
    "outputs": [
      {
        "internalType": "uint24",
        "name": "",
        "type": "uint24"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "hook",
    "outputs": [
      {
        "internalType": "contract IHooks",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "manager",
    "outputs": [
      {
        "internalType": "contract IPoolManager",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "pension",
    "outputs": [
      {
        "internalType": "contract IERC20",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "poolKey",
    "outputs": [
      {
        "components": [
          {
            "internalType": "Currency",
            "name": "currency0",
            "type": "address"
          },
          {
            "internalType": "Currency",
            "name": "currency1",
            "type": "address"
          },
          {
            "internalType": "uint24",
            "name": "fee",
            "type": "uint24"
          },
          {
            "internalType": "int24",
            "name": "tickSpacing",
            "type": "int24"
          },
          {
            "internalType": "contract IHooks",
            "name": "hooks",
            "type": "address"
          }
        ],
        "internalType": "struct PoolKey",
        "name": "",
        "type": "tuple"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "components": [
          {
            "internalType": "Currency",
            "name": "currency0",
            "type": "address"
          },
          {
            "internalType": "Currency",
            "name": "currency1",
            "type": "address"
          },
          {
            "internalType": "uint24",
            "name": "fee",
            "type": "uint24"
          },
          {
            "internalType": "int24",
            "name": "tickSpacing",
            "type": "int24"
          },
          {
            "internalType": "contract IHooks",
            "name": "hooks",
            "type": "address"
          }
        ],
        "internalType": "struct PoolKey",
        "name": "key",
        "type": "tuple"
      },
      {
        "internalType": "uint256",
        "name": "pensionIn",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "minEthOut",
        "type": "uint256"
      },
      {
        "internalType": "address",
        "name": "recipient",
        "type": "address"
      }
    ],
    "name": "sell",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "ethOut",
        "type": "uint256"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "tickSpacing",
    "outputs": [
      {
        "internalType": "int24",
        "name": "",
        "type": "int24"
      }
    ],
    "stateMutability": "view",
    "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 {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 {TickMath} from "v4-core/libraries/TickMath.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";
import {TransientStateLibrary} from "v4-core/libraries/TransientStateLibrary.sol";

contract V4PensionSwapRouter is IUnlockCallback {
    using CurrencyLibrary for Currency;
    using SafeERC20 for IERC20;
    using TransientStateLibrary for IPoolManager;

    uint256 public constant TAX_BIPS = 400;
    uint256 public constant BIPS = 10_000;

    IPoolManager public immutable manager;
    IERC20 public immutable pension;
    IHooks public immutable hook;
    uint24 public immutable fee;
    int24 public immutable tickSpacing;

    enum Action {
        Buy,
        Sell
    }

    struct CallbackData {
        Action action;
        address payer;
        address recipient;
        uint256 amountIn;
    }

    event RouterSwap(
        address indexed payer,
        address indexed recipient,
        bool indexed buy,
        uint256 amountIn,
        uint256 amountOut,
        uint256 taxEth
    );

    error InsufficientOutput();
    error InvalidAmount();
    error InvalidPool();
    error NotPoolManager();
    error RefundFailed();
    error ZeroAddress();

    constructor(
        IPoolManager poolManager,
        IERC20 pensionToken,
        IHooks pensionHook,
        uint24 poolFee,
        int24 poolTickSpacing
    ) {
        manager = poolManager;
        pension = pensionToken;
        hook = pensionHook;
        fee = poolFee;
        tickSpacing = poolTickSpacing;
    }

    receive() external payable {}

    function poolKey() public view returns (PoolKey memory) {
        return PoolKey({
            currency0: CurrencyLibrary.ADDRESS_ZERO,
            currency1: Currency.wrap(address(pension)),
            fee: fee,
            tickSpacing: tickSpacing,
            hooks: hook
        });
    }

    function buy(PoolKey calldata key, uint256 minPensionOut, address recipient)
        external
        payable
        returns (uint256 pensionOut)
    {
        _validatePoolKey(key);
        (pensionOut,) = _buyExactEthForPension(minPensionOut, recipient);
    }

    function sell(PoolKey calldata key, uint256 pensionIn, uint256 minEthOut, address recipient)
        external
        returns (uint256 ethOut)
    {
        _validatePoolKey(key);
        (ethOut,) = _sellExactPensionForEth(pensionIn, minEthOut, recipient);
    }

    function _buyExactEthForPension(uint256 minPensionOut, address recipient)
        internal
        returns (uint256 pensionOut, uint256 taxEth)
    {
        if (recipient == address(0)) revert ZeroAddress();
        if (msg.value == 0) revert InvalidAmount();
        (pensionOut, taxEth) = abi.decode(
            manager.unlock(abi.encode(CallbackData(Action.Buy, msg.sender, recipient, msg.value))), (uint256, uint256)
        );
        if (pensionOut < minPensionOut) revert InsufficientOutput();
        _refundEth(msg.sender);
        emit RouterSwap(msg.sender, recipient, true, msg.value, pensionOut, taxEth);
    }

    function _sellExactPensionForEth(uint256 pensionIn, uint256 minEthOut, address recipient)
        internal
        returns (uint256 ethOut, uint256 taxEth)
    {
        if (recipient == address(0)) revert ZeroAddress();
        if (pensionIn == 0) revert InvalidAmount();
        (ethOut, taxEth) = abi.decode(
            manager.unlock(abi.encode(CallbackData(Action.Sell, msg.sender, recipient, pensionIn))), (uint256, uint256)
        );
        if (ethOut < minEthOut) revert InsufficientOutput();
        _refundEth(msg.sender);
        emit RouterSwap(msg.sender, recipient, false, pensionIn, ethOut, taxEth);
    }

    function unlockCallback(bytes calldata rawData) external returns (bytes memory) {
        if (msg.sender != address(manager)) revert NotPoolManager();
        CallbackData memory data = abi.decode(rawData, (CallbackData));
        PoolKey memory key = poolKey();

        IPoolManager.SwapParams memory params = data.action == Action.Buy
            ? IPoolManager.SwapParams({
                zeroForOne: true,
                amountSpecified: -int256(data.amountIn),
                sqrtPriceLimitX96: TickMath.MIN_SQRT_PRICE + 1
            })
            : IPoolManager.SwapParams({
                zeroForOne: false,
                amountSpecified: -int256(data.amountIn),
                sqrtPriceLimitX96: TickMath.MAX_SQRT_PRICE - 1
            });

        manager.swap(key, params, "");

        int256 delta0 = manager.currencyDelta(address(this), key.currency0);
        int256 delta1 = manager.currencyDelta(address(this), key.currency1);

        if (delta0 < 0) manager.settle{value: uint256(-delta0)}();
        if (delta1 < 0) {
            uint256 tokenAmount = uint256(-delta1);
            manager.sync(key.currency1);
            pension.safeTransferFrom(data.payer, address(manager), tokenAmount);
            manager.settle();
        }

        uint256 ethOut;
        uint256 pensionOut;
        if (delta0 > 0) {
            ethOut = uint256(delta0);
            manager.take(key.currency0, data.recipient, ethOut);
        }
        if (delta1 > 0) {
            pensionOut = uint256(delta1);
            manager.take(key.currency1, data.recipient, pensionOut);
        }

        uint256 taxEth =
            data.action == Action.Buy ? data.amountIn * TAX_BIPS / BIPS : ethOut * TAX_BIPS / (BIPS - TAX_BIPS);

        return abi.encode(data.action == Action.Buy ? pensionOut : ethOut, taxEth);
    }

    function _validatePoolKey(PoolKey calldata key) internal view {
        PoolKey memory expected = poolKey();
        if (
            !(key.currency0 == expected.currency0) || !(key.currency1 == expected.currency1) || key.fee != expected.fee
                || key.tickSpacing != expected.tickSpacing || address(key.hooks) != address(expected.hooks)
        ) {
            revert InvalidPool();
        }
    }

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