HoodlSwapRouter

0xa92b26bdac5cb1ff4d22a8a36341d9011b5cdc03

Verification
Verified
v0.8.24+commit.e11b9ed9
Type
Contract
3,190 bytes
ABI entries
15
3 read · 3 write
License
none

Contract information

Address
0xa92b26bdac5cb1ff4d22a8a36341d9011b5cdc03
Chain
Robinhood Chain (4663)
Compiler
v0.8.24+commit.e11b9ed9
Optimization
Enabled
Creation tx
0x26a2b117b1…1612d73f57

Token

Not a token

This contract does not expose ERC-20 metadata.

Read contract (3)

hoodl()address
key()address, address, uint24, int24, address
poolManager()address

Events (2)

BoughtSold

ABI

[
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "poolManager_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "hoodl_",
        "type": "address"
      },
      {
        "internalType": "uint24",
        "name": "fee",
        "type": "uint24"
      },
      {
        "internalType": "int24",
        "name": "tickSpacing",
        "type": "int24"
      },
      {
        "internalType": "address",
        "name": "hook",
        "type": "address"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "inputs": [],
    "name": "Expired",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NativeTransferFailed",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "OnlyPoolManager",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "out",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "minOut",
        "type": "uint256"
      }
    ],
    "name": "Slippage",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ZeroAmount",
    "type": "error"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "buyer",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "nativeIn",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "hoodlOut",
        "type": "uint256"
      }
    ],
    "name": "Bought",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "seller",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "hoodlIn",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "nativeOut",
        "type": "uint256"
      }
    ],
    "name": "Sold",
    "type": "event"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "recipient",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "minHoodlOut",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "deadline",
        "type": "uint256"
      }
    ],
    "name": "buy",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "hoodlOut",
        "type": "uint256"
      }
    ],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "hoodl",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "key",
    "outputs": [
      {
        "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"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "poolManager",
    "outputs": [
      {
        "internalType": "contract IPoolManager",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "hoodlIn",
        "type": "uint256"
      },
      {
        "internalType": "address",
        "name": "recipient",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "minNativeOut",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "deadline",
        "type": "uint256"
      }
    ],
    "name": "sell",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "nativeOut",
        "type": "uint256"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes",
        "name": "raw",
        "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 { IPoolManager } from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
import { IHooks } from "@uniswap/v4-core/src/interfaces/IHooks.sol";
import { IUnlockCallback } from "@uniswap/v4-core/src/interfaces/callback/IUnlockCallback.sol";
import { TickMath } from "@uniswap/v4-core/src/libraries/TickMath.sol";
import { BalanceDelta, BalanceDeltaLibrary } from "@uniswap/v4-core/src/types/BalanceDelta.sol";
import { Currency } from "@uniswap/v4-core/src/types/Currency.sol";
import { PoolKey } from "@uniswap/v4-core/src/types/PoolKey.sol";
import { SwapParams } from "@uniswap/v4-core/src/types/PoolOperation.sol";

interface IERC20Minimal {
    function transfer(address to, uint256 amount) external returns (bool);
    function transferFrom(address from, address to, uint256 amount) external returns (bool);
}

/// @title HoodlSwapRouter
/// @notice Minimal single-pool exact-input router for the native/HOODL Uniswap v4
/// pool. One swap per call (so the hook's sell-tax provenance attributes to the real
/// seller), with slippage (minOut) and deadline protection.
contract HoodlSwapRouter is IUnlockCallback {
    using BalanceDeltaLibrary for BalanceDelta;

    // forge-lint: disable-next-line(screaming-snake-case-immutable)
    IPoolManager public immutable poolManager;
    // forge-lint: disable-next-line(screaming-snake-case-immutable)
    address public immutable hoodl;
    // forge-lint: disable-next-line(screaming-snake-case-immutable)
    PoolKey public key;

    error Expired();
    error OnlyPoolManager();
    error Slippage(uint256 out, uint256 minOut);
    error ZeroAmount();
    error NativeTransferFailed();

    event Bought(address indexed buyer, uint256 nativeIn, uint256 hoodlOut);
    event Sold(address indexed seller, uint256 hoodlIn, uint256 nativeOut);

    struct Callback {
        bool isBuy;
        address payer;
        address recipient;
        uint256 amountIn;
    }

    constructor(address poolManager_, address hoodl_, uint24 fee, int24 tickSpacing, address hook) {
        poolManager = IPoolManager(poolManager_);
        hoodl = hoodl_;
        // native (currency0) / HOODL (currency1) — native address(0) always sorts first.
        key = PoolKey({
            currency0: Currency.wrap(address(0)),
            currency1: Currency.wrap(hoodl_),
            fee: fee,
            tickSpacing: tickSpacing,
            hooks: IHooks(hook)
        });
    }

    receive() external payable { }

    /// @notice Buy HOODL with native ETH (exact input = msg.value).
    /// @param minHoodlOut Minimum HOODL to receive, or revert (slippage guard).
    function buy(address recipient, uint256 minHoodlOut, uint256 deadline)
        external
        payable
        returns (uint256 hoodlOut)
    {
        if (block.timestamp > deadline) revert Expired();
        if (msg.value == 0) revert ZeroAmount();
        bytes memory res = poolManager.unlock(
            abi.encode(
                Callback({
                    isBuy: true,
                    payer: msg.sender,
                    recipient: recipient,
                    amountIn: msg.value
                })
            )
        );
        hoodlOut = abi.decode(res, (uint256));
        if (hoodlOut < minHoodlOut) revert Slippage(hoodlOut, minHoodlOut);
        emit Bought(msg.sender, msg.value, hoodlOut);
    }

    /// @notice Sell HOODL for native ETH (exact input). Approve this router for
    /// `hoodlIn` first. `minNativeOut` is what lands after the hook's sell tax;
    /// any decayed-rate refund is credited to the seller separately by the game.
    function sell(uint256 hoodlIn, address recipient, uint256 minNativeOut, uint256 deadline)
        external
        returns (uint256 nativeOut)
    {
        if (block.timestamp > deadline) revert Expired();
        if (hoodlIn == 0) revert ZeroAmount();
        // Pull the seller's HOODL up front; the swap settles it from this router,
        // and the game's custody provenance traces the sell back to msg.sender.
        if (!IERC20Minimal(hoodl).transferFrom(msg.sender, address(this), hoodlIn)) {
            revert NativeTransferFailed();
        }
        bytes memory res = poolManager.unlock(
            abi.encode(
                Callback({
                    isBuy: false,
                    payer: msg.sender,
                    recipient: recipient,
                    amountIn: hoodlIn
                })
            )
        );
        nativeOut = abi.decode(res, (uint256));
        if (nativeOut < minNativeOut) revert Slippage(nativeOut, minNativeOut);
        emit Sold(msg.sender, hoodlIn, nativeOut);
    }

    function unlockCallback(bytes calldata raw) external returns (bytes memory) {
        if (msg.sender != address(poolManager)) revert OnlyPoolManager();
        Callback memory cb = abi.decode(raw, (Callback));

        BalanceDelta delta = poolManager.swap(
            key,
            SwapParams({
                zeroForOne: cb.isBuy,
                amountSpecified: -int256(cb.amountIn),
                sqrtPriceLimitX96: cb.isBuy
                    ? TickMath.MIN_SQRT_PRICE + 1
                    : TickMath.MAX_SQRT_PRICE - 1
            }),
            ""
        );

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

        if (cb.isBuy) {
            // owe native (settle from msg.value the router holds), receive HOODL
            if (nativeDelta < 0) {
                poolManager.settle{ value: uint256(uint128(-nativeDelta)) }();
            }
            uint256 out = hoodlDelta > 0 ? uint256(uint128(hoodlDelta)) : 0;
            if (out > 0) poolManager.take(key.currency1, cb.recipient, out);
            return abi.encode(out);
        }

        // sell: owe HOODL (settle from this router's balance), receive native
        if (hoodlDelta < 0) {
            uint256 owed = uint256(uint128(-hoodlDelta));
            poolManager.sync(key.currency1);
            IERC20Minimal(hoodl).transfer(address(poolManager), owed);
            poolManager.settle();
        }
        uint256 nativeOut = nativeDelta > 0 ? uint256(uint128(nativeDelta)) : 0;
        if (nativeOut > 0) poolManager.take(key.currency0, cb.recipient, nativeOut);
        return abi.encode(nativeOut);
    }
}
Chain explorer5031msChain node87ms