PadQuiverFactory

0xfd15460de2610ab1dbd990efc668b79b4159196e

Verification
Verified
v0.8.24+commit.e11b9ed9
Type
Contract
13,625 bytes
ABI entries
31
18 read · 7 write
License
none

Contract information

Address
0xfd15460de2610ab1dbd990efc668b79b4159196e
Chain
Robinhood Chain (4663)
Compiler
v0.8.24+commit.e11b9ed9
Optimization
Enabled
Creation tx
0xda50624fc1…1a605f99da

Token

Not a token

This contract does not expose ERC-20 metadata.

Read contract (18)

WEBSITE()string
computeSaltHash(address, bytes32)bytes32
creationFee()uint256
defaultCallerTipBps()uint256
defaultMaxSlippageBps()uint256
defaultPlatformFeeBps()uint256
defaultTwapWindow()uint32
distributorDeployer()address
distributorOf(address)address
feeRecipient()address
observationCardinality()uint16
owner()address
positionManager()address
swapRouter()address
tokens(uint256)address, address, address, address, address, uint256
tokensCount()uint256
v3Factory()address
weth()address

Events (2)

OwnershipTransferredTokenCreated

ABI

[
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "v3Factory_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "positionManager_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "swapRouter_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "weth_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "feeRecipient_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "distributorDeployer_",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "creationFee_",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "defaultPlatformFeeBps_",
        "type": "uint256"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "owner",
        "type": "address"
      }
    ],
    "name": "OwnableInvalidOwner",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "account",
        "type": "address"
      }
    ],
    "name": "OwnableUnauthorizedAccount",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ReentrancyGuardReentrantCall",
    "type": "error"
  },
  {
    "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": "creator",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "address",
        "name": "distributor",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "address",
        "name": "pool",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "address",
        "name": "rewardToken",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "positionTokenId",
        "type": "uint256"
      }
    ],
    "name": "TokenCreated",
    "type": "event"
  },
  {
    "inputs": [],
    "name": "WEBSITE",
    "outputs": [
      {
        "internalType": "string",
        "name": "",
        "type": "string"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "creator",
        "type": "address"
      },
      {
        "internalType": "bytes32",
        "name": "salt",
        "type": "bytes32"
      }
    ],
    "name": "computeSaltHash",
    "outputs": [
      {
        "internalType": "bytes32",
        "name": "",
        "type": "bytes32"
      }
    ],
    "stateMutability": "pure",
    "type": "function"
  },
  {
    "inputs": [
      {
        "components": [
          {
            "internalType": "string",
            "name": "name",
            "type": "string"
          },
          {
            "internalType": "string",
            "name": "symbol",
            "type": "string"
          },
          {
            "internalType": "uint256",
            "name": "supply",
            "type": "uint256"
          },
          {
            "internalType": "address",
            "name": "rewardToken",
            "type": "address"
          },
          {
            "internalType": "uint24",
            "name": "poolFee",
            "type": "uint24"
          },
          {
            "internalType": "uint24",
            "name": "rewardPoolFee",
            "type": "uint24"
          },
          {
            "internalType": "uint160",
            "name": "sqrtPriceX96",
            "type": "uint160"
          },
          {
            "internalType": "int24",
            "name": "tickLower",
            "type": "int24"
          },
          {
            "internalType": "int24",
            "name": "tickUpper",
            "type": "int24"
          },
          {
            "internalType": "bytes32",
            "name": "salt",
            "type": "bytes32"
          },
          {
            "internalType": "uint256",
            "name": "minRewardBalance",
            "type": "uint256"
          }
        ],
        "internalType": "struct PadQuiverFactory.CreateParams",
        "name": "p",
        "type": "tuple"
      }
    ],
    "name": "createToken",
    "outputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "distributor",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "pool",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      }
    ],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "creationFee",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "defaultCallerTipBps",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "defaultMaxSlippageBps",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "defaultPlatformFeeBps",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "defaultTwapWindow",
    "outputs": [
      {
        "internalType": "uint32",
        "name": "",
        "type": "uint32"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "distributorDeployer",
    "outputs": [
      {
        "internalType": "contract PadQuiverDeployer",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "distributorOf",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "feeRecipient",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "observationCardinality",
    "outputs": [
      {
        "internalType": "uint16",
        "name": "",
        "type": "uint16"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "owner",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "positionManager",
    "outputs": [
      {
        "internalType": "contract INonfungiblePositionManager",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "renounceOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "fee",
        "type": "uint256"
      }
    ],
    "name": "setCreationFee",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "bps",
        "type": "uint256"
      }
    ],
    "name": "setDefaultPlatformFeeBps",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "r",
        "type": "address"
      }
    ],
    "name": "setFeeRecipient",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint32",
        "name": "window",
        "type": "uint32"
      },
      {
        "internalType": "uint256",
        "name": "maxSlippageBps",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "callerTipBps",
        "type": "uint256"
      },
      {
        "internalType": "uint16",
        "name": "cardinality",
        "type": "uint16"
      }
    ],
    "name": "setTwapDefaults",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "swapRouter",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "name": "tokens",
    "outputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "distributor",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "pool",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "creator",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "rewardToken",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "positionTokenId",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "tokensCount",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "transferOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "v3Factory",
    "outputs": [
      {
        "internalType": "contract IUniswapV3Factory",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "weth",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  }
]

Source code

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

import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import {PadQuiverToken} from "./PadQuiverToken.sol";
import {PadQuiverDistributor} from "./PadQuiverDistributor.sol";
import {PadQuiverDeployer} from "./PadQuiverDeployer.sol";
import {IUniswapV3Factory, IUniswapV3Pool, INonfungiblePositionManager, ISwapRouter, IWETH9} from "./interfaces/IUniswapV3.sol";

/**
 * @title PadQuiverFactory
 * @author PadQuiver Launchpad — https://padquiver.com/
 * @custom:website https://padquiver.com/
 * @notice The heart of the PadQuiver launchpad. Creates a customer's reward-bearing token:
 *         the factory mints the token, opens a Uniswap V3 pool, places the entire supply
 *         into a SINGLE-SIDED (capital-free) position, and hands the NFT to the reward
 *         distributor. Liquidity is permanently locked; only fees are collected and paid
 *         out as rewards.
 *
 *         SECURITY (audit fixes):
 *         - The token is deployed via CREATE2 (salt = keccak(creator, userSalt)); the address
 *           cannot be predicted by an attacker, and if it gets poisoned a new salt lets you
 *           escape it (closing the permanent-brick DoS).
 *         - After the pool is created, slot0.tick is VALIDATED: if the price is not on the
 *           single-sided edge it reverts loudly (preventing a silent price override and a
 *           wrong-ratio mint).
 *         - Tick alignment (tickSpacing) is validated.
 *         - Leftover tokens are burned; any overpaid creation fee is refunded.
 */
contract PadQuiverFactory is Ownable, ReentrancyGuard {
    /// @notice Official PadQuiver launchpad website (readable on-chain).
    string public constant WEBSITE = "https://padquiver.com/";

    address private constant DEAD = 0x000000000000000000000000000000000000dEaD;

    IUniswapV3Factory public immutable v3Factory;
    INonfungiblePositionManager public immutable positionManager;
    address public immutable swapRouter;
    address public immutable weth;
    PadQuiverDeployer public immutable distributorDeployer;

    uint256 public creationFee;
    address public feeRecipient;
    uint256 public defaultPlatformFeeBps;

    // TWAP protection parameters for bot-free harvesting (applied to new tokens)
    uint32 public defaultTwapWindow = 600; // 10 min
    uint256 public defaultMaxSlippageBps = 500; // 5%
    uint256 public defaultCallerTipBps = 25; // 0.25% (to whoever calls harvest)
    uint16 public observationCardinality = 120; // pool oracle buffer size

    struct TokenRecord {
        address token;
        address distributor;
        address pool;
        address creator;
        address rewardToken;
        uint256 positionTokenId;
    }

    TokenRecord[] public tokens;
    mapping(address => address) public distributorOf;

    event TokenCreated(
        address indexed creator,
        address indexed token,
        address distributor,
        address pool,
        address rewardToken,
        uint256 positionTokenId
    );

    struct CreateParams {
        string name;
        string symbol;
        uint256 supply;
        address rewardToken;
        uint24 poolFee;
        uint24 rewardPoolFee;
        uint160 sqrtPriceX96;
        int24 tickLower;
        int24 tickUpper;
        bytes32 salt; // CREATE2 salt (client-chosen; makes the address unpredictable)
        uint256 minRewardBalance; // minimum holder balance to earn rewards (0 = default; high = only large holders)
    }

    constructor(
        address v3Factory_,
        address positionManager_,
        address swapRouter_,
        address weth_,
        address feeRecipient_,
        address distributorDeployer_,
        uint256 creationFee_,
        uint256 defaultPlatformFeeBps_
    ) Ownable(msg.sender) {
        require(defaultPlatformFeeBps_ <= 2_000, "RRF: platform fee too high");
        require(distributorDeployer_ != address(0), "RRF: zero deployer");
        v3Factory = IUniswapV3Factory(v3Factory_);
        positionManager = INonfungiblePositionManager(positionManager_);
        swapRouter = swapRouter_;
        weth = weth_;
        feeRecipient = feeRecipient_;
        distributorDeployer = PadQuiverDeployer(distributorDeployer_);
        creationFee = creationFee_;
        defaultPlatformFeeBps = defaultPlatformFeeBps_;
    }

    function createToken(CreateParams calldata p)
        external
        payable
        nonReentrant
        returns (address token, address distributor, address pool, uint256 tokenId)
    {
        uint256 fee = feeRecipient == address(0) ? 0 : creationFee; // no fee charged when feeRecipient is unset
        require(msg.value >= fee, "RRF: creation fee");
        require(p.rewardToken != address(0), "RRF: zero reward token");
        require(p.supply > 0, "RRF: zero supply");
        require(p.tickLower < p.tickUpper, "RRF: bad ticks");
        // Reward threshold: 0 = default (~0.001% of supply); at most 10% of supply (avoids excluding everyone)
        uint256 minRewardBalance = p.minRewardBalance == 0 ? p.supply / 100_000 : p.minRewardBalance;
        require(minRewardBalance <= p.supply / 10, "RRF: reward threshold too high");
        // The reward coin MUST have a WETH pool, otherwise harvest cannot convert rewards (stranded WETH)
        address rewardPool;
        if (p.rewardToken != weth) {
            rewardPool = v3Factory.getPool(weth, p.rewardToken, p.rewardPoolFee);
            require(rewardPool != address(0), "RRF: no reward/WETH pool");
        }

        // 1) Mint the token via CREATE2 (unpredictable address; escape with a new salt if poisoned)
        bytes32 saltHash = keccak256(abi.encodePacked(msg.sender, p.salt));
        PadQuiverToken t = new PadQuiverToken{salt: saltHash}(
            p.name, p.symbol, p.supply, address(this), msg.sender
        );
        token = address(t);
        require(token != weth && token != p.rewardToken, "RRF: token clash");

        bool tokenIsToken0 = token < weth;
        (address token0, address token1) = tokenIsToken0 ? (token, weth) : (weth, token);

        // 2) Create the pool + set the initial price
        pool = v3Factory.getPool(token0, token1, p.poolFee);
        bool preExisted = pool != address(0);
        if (!preExisted) {
            pool = v3Factory.createPool(token0, token1, p.poolFee);
            // Fresh pool: initialize DIRECTLY — let a bad sqrtPriceX96 revert loudly
            IUniswapV3Pool(pool).initialize(p.sqrtPriceX96);
        } else {
            // Already exists (rare/adversarial): swallow the 'already initialized' revert;
            // the slot0 check below still validates the price
            try IUniswapV3Pool(pool).initialize(p.sqrtPriceX96) {} catch {}
        }

        // 2b) VALIDATE: tick alignment + price on the single-sided edge (front-run/override protection)
        int24 spacing = IUniswapV3Pool(pool).tickSpacing();
        require(p.tickLower % spacing == 0 && p.tickUpper % spacing == 0, "RRF: tick misaligned");
        (, int24 currentTick,,,,,) = IUniswapV3Pool(pool).slot0();
        if (tokenIsToken0) {
            require(currentTick < p.tickLower, "RRF: price not below range");
        } else {
            require(currentTick >= p.tickUpper, "RRF: price not above range");
        }

        // 2c) Grow the oracle buffer — so TWAP protection can accumulate history
        //     (both the token pool and the reward pool; the reward pool may have cardinality=1)
        IUniswapV3Pool(pool).increaseObservationCardinalityNext(observationCardinality);
        if (rewardPool != address(0)) {
            IUniswapV3Pool(rewardPool).increaseObservationCardinalityNext(observationCardinality);
        }

        // 3) Set up the distributor (via a separate deployer — for the factory's code-size limit).
        //    Config.factory points to this factory (not the deployer).
        distributor = distributorDeployer.deploy(
            PadQuiverDistributor.Config({
                factory: address(this),
                token: token,
                positionManager: address(positionManager),
                swapRouter: swapRouter,
                weth: weth,
                rewardToken: p.rewardToken,
                pool: pool,
                rewardPool: rewardPool,
                poolFee: p.poolFee,
                rewardPoolFee: p.rewardPoolFee,
                tokenIsToken0: tokenIsToken0,
                platformWallet: feeRecipient,
                platformFeeBps: defaultPlatformFeeBps,
                twapWindow: defaultTwapWindow,
                maxSlippageBps: defaultMaxSlippageBps,
                callerTipBps: defaultCallerTipBps,
                minimumBalanceForDividends: minRewardBalance
            })
        );
        t.initializeDistributor(distributor);

        // 4) Place the entire supply into a single-sided position; the NFT goes to the distributor
        t.approve(address(positionManager), p.supply);
        (tokenId,,,) = positionManager.mint(
            INonfungiblePositionManager.MintParams({
                token0: token0,
                token1: token1,
                fee: p.poolFee,
                tickLower: p.tickLower,
                tickUpper: p.tickUpper,
                amount0Desired: tokenIsToken0 ? p.supply : 0,
                amount1Desired: tokenIsToken0 ? 0 : p.supply,
                amount0Min: 0,
                amount1Min: 0,
                recipient: distributor,
                deadline: block.timestamp
            })
        );
        PadQuiverDistributor(distributor).initialize(tokenId);

        // 4b) Burn leftover tokens (rounding dust) — so they aren't locked in the factory
        uint256 leftover = t.balanceOf(address(this));
        if (leftover > 0) {
            t.transfer(DEAD, leftover);
        }

        // 5) Record
        tokens.push(
            TokenRecord({
                token: token,
                distributor: distributor,
                pool: pool,
                creator: msg.sender,
                rewardToken: p.rewardToken,
                positionTokenId: tokenId
            })
        );
        distributorOf[token] = distributor;

        // 6) Fee: charge the fee (0 if there is no feeRecipient). If any ETH remains, make the FIRST
        //    BUY on the creator's behalf (pump.fun-style "dev buy"): since the pool + position are
        //    created in this tx, the buy is ATOMIC and no one can front-run or snipe it. minOut=0 is
        //    safe (same tx, sandwiching impossible; the creator accepts the launch price). The
        //    purchased tokens go to the creator and are written into the reward ledger.
        if (fee > 0) {
            (bool okFee,) = feeRecipient.call{value: fee}("");
            require(okFee, "RRF: fee transfer failed");
        }
        uint256 devBuyEth = msg.value - fee;
        if (devBuyEth > 0) {
            IWETH9(weth).deposit{value: devBuyEth}();
            IWETH9(weth).approve(swapRouter, devBuyEth);
            ISwapRouter(swapRouter).exactInputSingle(
                ISwapRouter.ExactInputSingleParams({
                    tokenIn: weth,
                    tokenOut: token,
                    fee: p.poolFee,
                    recipient: msg.sender,
                    deadline: block.timestamp,
                    amountIn: devBuyEth,
                    amountOutMinimum: 0,
                    sqrtPriceLimitX96: 0
                })
            );
        }

        emit TokenCreated(msg.sender, token, distributor, pool, p.rewardToken, tokenId);
    }

    function tokensCount() external view returns (uint256) {
        return tokens.length;
    }

    /// @notice Helps precompute the next token's CREATE2 address (for the client's tick calculation).
    function computeSaltHash(address creator, bytes32 salt) external pure returns (bytes32) {
        return keccak256(abi.encodePacked(creator, salt));
    }

    // --- Admin ---
    function setCreationFee(uint256 fee) external onlyOwner {
        creationFee = fee;
    }

    function setFeeRecipient(address r) external onlyOwner {
        feeRecipient = r;
    }

    function setDefaultPlatformFeeBps(uint256 bps) external onlyOwner {
        require(bps <= 2_000, "RRF: too high");
        defaultPlatformFeeBps = bps;
    }

    /// @notice TWAP protection parameters applied to new tokens (does not affect existing tokens).
    function setTwapDefaults(uint32 window, uint256 maxSlippageBps, uint256 callerTipBps, uint16 cardinality)
        external
        onlyOwner
    {
        require(window >= 60 && window <= 3600, "RRF: bad window");
        require(maxSlippageBps <= 1_000, "RRF: slippage too high");
        require(callerTipBps <= 100, "RRF: tip too high");
        require(cardinality >= 2, "RRF: bad cardinality");
        defaultTwapWindow = window;
        defaultMaxSlippageBps = maxSlippageBps;
        defaultCallerTipBps = callerTipBps;
        observationCardinality = cardinality;
    }
}
Chain explorer3729msChain node91ms