Robinhood MainnetChain ID: 4663
Contract detail

PRNTR

PRNTR is a verified Robinhood Chain smart contract at 0x4e9bd55d…b97ed124 with 7 indexed transactions.

Address type
Smart contract
Transactions
7
First indexed block
#28691656
Latest indexed block
#28709322
Indexed token transfers
14
ERC-20 transfers
14
Emitted events
7
Verification
Verified
Compiler
v0.8.26+commit.8a97fa7a
Verification match
full
Creation transaction
0x74e2bebe…2692e8f6
Created at block
#28609479
Language
solidity
EVM version
cancun
License
none
Compiler optimization
Enabled · 200 runs
Related activity

Transactions

Verified contract

Contract source code

// 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 {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol";
import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol";
import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol";
import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol";
import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol";
import {FullMath} from "@uniswap/v4-core/src/libraries/FullMath.sol";
import {SafeERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import {PRNTRToken} from "./PRNTRToken.sol";
import {IHookRegistry} from "./IHookRegistry.sol";

address constant WETH = 0x0Bd7D308f8E1639FAb988df18A8011f41EAcAD73;

interface IWETH {
    function deposit() external payable;
    function withdraw(uint256) external;
}

contract PRNTR is ReentrancyGuard {
    using CurrencyLibrary for Currency;
    using PoolIdLibrary for PoolKey;
    using SafeERC20 for IERC20;

    // errors
    error OnlyOwner();
    error OnlyPoolManager();
    error UnknownToken();
    error NoFeesToCollect();
    error ETHTransferFailed();
    error SqrtPriceOverflow();
    error StartingEthOutOfRange();
    error SweepRecipientNotSet();
    error ZeroAddress();
    error SettleMismatch();

    // constants
    int24 public constant TICK_SPACING = 60;
    int24 public constant TICK_LOWER = -887220;
    int24 public constant TICK_UPPER_MAX = 887220;
    uint256 public constant SUPPLY = 1_000_000_000 * 10 ** 18;
    uint256 public constant CREATOR_FEE_BPS = 3000;

    // immutables
    IPoolManager public immutable poolManager;
    address public immutable hookAddress;
    address public immutable owner;

    // state
    address public sweepRecipient;

    struct TokenInfo {
        address creator;
        int24 tickLower;
        int24 tickUpper;
        uint128 liquidity;
        bool wethIsZero;
    }

    mapping(address => TokenInfo) public tokenInfo;

    // events
    event TokenPrinted(
        address indexed tokenAddress,
        address indexed creator,
        uint256 startingEth,
        uint24 feeTier,
        uint160 sqrtPriceX96,
        PoolId poolId,
        uint256 creatorBuyETH,
        uint256 creatorTokensReceived,
        bool sniperEnabled,
        uint256 lockExpiry
    );

    event FeesCollected(address indexed tokenAddress, uint256 ethToCreator, uint256 ethToPlatform);

    // callback data
    struct CallbackData {
        PoolKey key;
        address tokenAddress;
        int256 liquidityDelta;
        int24 tickLower;
        int24 tickUpper;
        uint256 creatorBuyETH;
        address creator;
        bool wethIsZero;
    }

    constructor(address _poolManager, address _hookAddress) {
        if (_poolManager == address(0) || _hookAddress == address(0)) revert ZeroAddress();
        poolManager = IPoolManager(_poolManager);
        hookAddress = _hookAddress;
        owner = msg.sender;
    }

    // admin
    function setSweepRecipient(address _recipient) external {
        if (msg.sender != owner) revert OnlyOwner();
        if (_recipient == address(0)) revert ZeroAddress();
        sweepRecipient = _recipient;
    }

    function sweepETH() external {
        if (msg.sender != owner) revert OnlyOwner();
        if (sweepRecipient == address(0)) revert SweepRecipientNotSet();
        uint256 bal = address(this).balance;
        if (bal > 0) {
            (bool ok,) = sweepRecipient.call{value: bal}("");
            if (!ok) revert ETHTransferFailed();
        }
    }

    function sweepToken(address token) external {
        if (msg.sender != owner) revert OnlyOwner();
        if (sweepRecipient == address(0)) revert SweepRecipientNotSet();
        uint256 bal = IERC20(token).balanceOf(address(this));
        if (bal > 0) IERC20(token).safeTransfer(sweepRecipient, bal);
    }

    // pool key - sorts currencies so currency0 < currency1
    function buildKey(address token) public view returns (PoolKey memory key, bool wethIsZero) {
        wethIsZero = WETH < token;
        key = PoolKey({
            currency0: Currency.wrap(wethIsZero ? WETH : token),
            currency1: Currency.wrap(wethIsZero ? token : WETH),
            fee: 0x800000,
            tickSpacing: TICK_SPACING,
            hooks: IHooks(hookAddress)
        });
    }

    // price math
    function getSqrtPriceX96(uint256 startingEth, bool wethIsZero) public pure returns (uint160) {
        if (startingEth < 1 ether || startingEth > 10 ether) revert StartingEthOutOfRange();
        uint256 numerator = 15 ether;
        uint256 ratio = FullMath.mulDiv(numerator, 1e18, startingEth * 10);
        uint256 sqrtRatio = _sqrt(ratio);
        uint256 sqrtPrice = FullMath.mulDiv(uint256(25820) * sqrtRatio, (1 << 96), 1e9);
        if (!wethIsZero) {
            // token is currency0: invert price
            sqrtPrice = FullMath.mulDiv(1 << 96, 1 << 96, sqrtPrice);
        }
        if (sqrtPrice > type(uint160).max) revert SqrtPriceOverflow();
        return uint160(sqrtPrice);
    }

    function _sqrt(uint256 x) internal pure returns (uint256) {
        if (x == 0) return 0;
        uint256 z = (x + 1) / 2;
        uint256 y = x;
        while (z < y) {
            y = z;
            z = (x / z + z) / 2;
        }
        return y;
    }

    // tick range - handles both orderings with correct floor/ceil rounding
    function _liquidityFor(uint160 sqrtPriceX96, bool wethIsZero)
        internal
        pure
        returns (int24 tLower, int24 tUpper, uint128 liquidity)
    {
        int24 currentTick = TickMath.getTickAtSqrtPrice(sqrtPriceX96);

        if (wethIsZero) {
            // WETH=c0, token=c1: range [TICK_LOWER, floor(currentTick)]
            int24 floored = currentTick / TICK_SPACING;
            if (currentTick < 0 && currentTick % TICK_SPACING != 0) floored -= 1;
            tLower = TICK_LOWER;
            tUpper = floored * TICK_SPACING;
            uint160 sqrtA = TickMath.getSqrtPriceAtTick(tLower);
            uint160 sqrtB = TickMath.getSqrtPriceAtTick(tUpper);
            liquidity = uint128(FullMath.mulDiv(SUPPLY, 2 ** 96, sqrtB - sqrtA));
        } else {
            // token=c0, WETH=c1: range [strictly above currentTick, TICK_UPPER_MAX]
            // Position sits entirely above current price, so it holds pure token0 (the
            // printed token). That needs the token0 liquidity formula, not the token1 one:
            // amount0 = L*(sqrtB-sqrtA)/(sqrtA*sqrtB)  =>  L = amount0*sqrtA*sqrtB/(sqrtB-sqrtA)
            //
            // Single-sidedness requires currentTick < tLower (strict), so tLower must
            // be the smallest tick-spacing multiple strictly greater than currentTick -
            // i.e. floor(currentTick) plus one full spacing step, even when currentTick
            // already sits exactly on a spacing boundary (plain ceil(currentTick) would
            // equal currentTick itself there, which only satisfies the non-strict `<=`
            // that the *other* branch is allowed to use, not this one).
            int24 floored = currentTick / TICK_SPACING;
            if (currentTick < 0 && currentTick % TICK_SPACING != 0) floored -= 1;
            tLower = (floored + 1) * TICK_SPACING;
            tUpper = TICK_UPPER_MAX;
            uint160 sqrtA = TickMath.getSqrtPriceAtTick(tLower);
            uint160 sqrtB = TickMath.getSqrtPriceAtTick(tUpper);
            uint256 intermediate = FullMath.mulDiv(sqrtA, sqrtB, 2 ** 96);
            liquidity = uint128(FullMath.mulDiv(SUPPLY, intermediate, sqrtB - sqrtA));
        }
    }

    // print token
    function printToken(
        string calldata name,
        string calldata symbol,
        uint256 startingEth,
        uint24 feeTier,
        bool enableSniper,
        uint256 lockDuration
    ) external payable nonReentrant returns (address) {
        PRNTRToken token = new PRNTRToken(name, symbol, SUPPLY, address(this));
        (PoolKey memory key, bool wethIsZero) = buildKey(address(token));
        uint160 sqrtPrice = getSqrtPriceX96(startingEth, wethIsZero);
        poolManager.initialize(key, sqrtPrice);
        PoolId poolId = key.toId();
        IHookRegistry(hookAddress).registerPool(poolId, feeTier, enableSniper);
        (int24 tLower, int24 tUpper, uint128 liquidity) = _liquidityFor(sqrtPrice, wethIsZero);
        tokenInfo[address(token)] = TokenInfo({
            creator: msg.sender, tickLower: tLower, tickUpper: tUpper, liquidity: liquidity, wethIsZero: wethIsZero
        });
        bytes memory result = poolManager.unlock(
            abi.encode(
                CallbackData({
                    key: key,
                    tokenAddress: address(token),
                    liquidityDelta: int256(uint256(liquidity)),
                    tickLower: tLower,
                    tickUpper: tUpper,
                    creatorBuyETH: msg.value,
                    creator: msg.sender,
                    wethIsZero: wethIsZero
                })
            )
        );
        (uint256 creatorTokensReceived, uint256 ethUsed) = abi.decode(result, (uint256, uint256));
        // Refund unused ETH
        if (msg.value > ethUsed) {
            (bool ok,) = msg.sender.call{value: msg.value - ethUsed}("");
            if (!ok) revert ETHTransferFailed();
        }
        uint256 lockExpiry = 0;
        if (lockDuration > 0 && ethUsed > 0) {
            lockExpiry = block.timestamp + lockDuration;
            token.setLock(msg.sender, lockExpiry);
        }
        emit TokenPrinted(
            address(token),
            msg.sender,
            startingEth,
            feeTier,
            sqrtPrice,
            poolId,
            ethUsed,
            creatorTokensReceived,
            enableSniper,
            lockExpiry
        );
        return address(token);
    }

    // collect fees
    function collectFees(address token) external nonReentrant returns (uint256 ethFees) {
        TokenInfo memory info = tokenInfo[token];
        if (info.creator == address(0)) revert UnknownToken();
        (PoolKey memory key,) = buildKey(token);
        PoolId poolId = key.toId();
        ethFees = IHookRegistry(hookAddress).claimFees(poolId);
        if (ethFees == 0) revert NoFeesToCollect();
        IWETH(WETH).withdraw(ethFees);
        uint256 ethToCreator = (ethFees * CREATOR_FEE_BPS) / 10000;
        uint256 ethToPlatform = ethFees - ethToCreator;
        if (ethToCreator > 0) {
            (bool ok,) = info.creator.call{value: ethToCreator}("");
            if (!ok) revert ETHTransferFailed();
        }
        if (ethToPlatform > 0) {
            (bool ok2,) = owner.call{value: ethToPlatform}("");
            if (!ok2) revert ETHTransferFailed();
        }
        emit FeesCollected(token, ethToCreator, ethToPlatform);
    }

    // unlock callback
    function unlockCallback(bytes calldata rawData) external returns (bytes memory) {
        if (msg.sender != address(poolManager)) revert OnlyPoolManager();
        CallbackData memory data = abi.decode(rawData, (CallbackData));
        return _launch(data);
    }

    function _launch(CallbackData memory data) internal returns (bytes memory) {
        (BalanceDelta delta,) = poolManager.modifyLiquidity(
            data.key,
            IPoolManager.ModifyLiquidityParams({
                tickLower: data.tickLower,
                tickUpper: data.tickUpper,
                liquidityDelta: data.liquidityDelta,
                salt: bytes32(0)
            }),
            new bytes(0)
        );

        int256 a0 = int256(delta.amount0());
        int256 a1 = int256(delta.amount1());

        if (a0 < 0) {
            uint256 needed = uint256(-a0);
            poolManager.sync(data.key.currency0);
            if (data.wethIsZero) {
                IWETH(WETH).deposit{value: needed}();
                IERC20(WETH).safeTransfer(address(poolManager), needed);
            } else {
                IERC20(data.tokenAddress).safeTransfer(address(poolManager), needed);
            }
            if (poolManager.settle() != needed) revert SettleMismatch();
        }

        if (a1 < 0) {
            uint256 needed = uint256(-a1);
            poolManager.sync(data.key.currency1);
            if (!data.wethIsZero) {
                IWETH(WETH).deposit{value: needed}();
                IERC20(WETH).safeTransfer(address(poolManager), needed);
            } else {
                IERC20(data.tokenAddress).safeTransfer(address(poolManager), needed);
            }
            if (poolManager.settle() != needed) revert SettleMismatch();
        }

        uint256 creatorTokens = 0;
        uint256 ethUsed = 0;

        if (data.creatorBuyETH > 0) {
            bool zeroForOne = data.wethIsZero;
            BalanceDelta swapDelta = poolManager.swap(
                data.key,
                IPoolManager.SwapParams({
                    zeroForOne: zeroForOne,
                    amountSpecified: -int256(data.creatorBuyETH),
                    sqrtPriceLimitX96: zeroForOne ? TickMath.MIN_SQRT_PRICE + 1 : TickMath.MAX_SQRT_PRICE - 1
                }),
                new bytes(0)
            );

            int256 wethDelta = data.wethIsZero ? int256(swapDelta.amount0()) : int256(swapDelta.amount1());

            if (wethDelta < 0) {
                ethUsed = uint256(-wethDelta);
                IWETH(WETH).deposit{value: ethUsed}();
                Currency wethCur = data.wethIsZero ? data.key.currency0 : data.key.currency1;
                poolManager.sync(wethCur);
                IERC20(WETH).safeTransfer(address(poolManager), ethUsed);
                if (poolManager.settle() != ethUsed) revert SettleMismatch();
            }

            int256 tokenDelta = data.wethIsZero ? int256(swapDelta.amount1()) : int256(swapDelta.amount0());

            if (tokenDelta > 0) {
                creatorTokens = uint256(tokenDelta);
                Currency tokenCur = data.wethIsZero ? data.key.currency1 : data.key.currency0;
                poolManager.take(tokenCur, data.creator, creatorTokens);
            }
        }

        return abi.encode(creatorTokens, ethUsed);
    }

    receive() external payable {}
}