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 {}
}