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
- Creator
- 0x1149ac0269…6994Dd16DA
- 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