RawbinInstantDEX
0x9ee7e27ac6f3028a93960dc53f30c9c905010999
Verification
Verified
v0.8.24+commit.e11b9ed9
Type
Contract
14,291 bytes
ABI entries
47
27 read · 9 write
License
none
Contract information
- Address
- 0x9ee7e27ac6f3028a93960dc53f30c9c905010999
- Chain
- Robinhood Chain (4663)
- Compiler
- v0.8.24+commit.e11b9ed9
- Optimization
- Enabled
- Creator
- 0xE9DD94f36E…0a06821862
- Creation tx
- 0x2c42c158be…1ff3a7d74d
Token
Not a token
This contract does not expose ERC-20 metadata.
Read contract (27)
FEE_TIMELOCK() → uint256
GRADUATION_WETH() → uint256
MAX_CREATION_FEE() → uint256
PLATFORM() → string
PLATFORM_URL() → string
TICK_SPACING() → int24
TOTAL_SUPPLY() → uint256
V3_FEE() → uint24
allTokens(uint256) → address, address, uint256, address, string, uint256
allTokensLength() → uint256
creationFee() → uint256
feeRecipient() → address
feeRecipientEta() → uint256
graduated(address) → bool
isCurve(address) → bool
locker() → address
npm() → address
owner() → address
pendingFeeRecipient() → address
pendingOwner() → address
poolOf(address) → address
predictToken(address, string, string, bytes32) → address
sqrtPriceStartX96() → uint160
swapRouter() → address
tickLower() → int24
tickUpper() → int24
weth() → address
Events (8)
CreationFeeUpdatedFeeRecipientCancelledFeeRecipientProposedFeeRecipientUpdatedGraduatedOwnershipTransferStartedOwnershipTransferredTokenCreated
ABI
[
{
"inputs": [
{
"internalType": "address",
"name": "feeRecipient_",
"type": "address"
},
{
"internalType": "contract INonfungiblePositionManager",
"name": "npm_",
"type": "address"
},
{
"internalType": "address",
"name": "swapRouter_",
"type": "address"
},
{
"internalType": "uint160",
"name": "sqrtPriceStartX96_",
"type": "uint160"
},
{
"internalType": "int24",
"name": "tickLower_",
"type": "int24"
},
{
"internalType": "int24",
"name": "tickUpper_",
"type": "int24"
},
{
"internalType": "uint256",
"name": "creationFee_",
"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"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "uint256",
"name": "newFee",
"type": "uint256"
}
],
"name": "CreationFeeUpdated",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "cancelled",
"type": "address"
}
],
"name": "FeeRecipientCancelled",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "next",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "eta",
"type": "uint256"
}
],
"name": "FeeRecipientProposed",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "prev",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "next",
"type": "address"
}
],
"name": "FeeRecipientUpdated",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "pool",
"type": "address"
}
],
"name": "Graduated",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "previousOwner",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "OwnershipTransferStarted",
"type": "event"
},
{
"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": "token",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "pool",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "creator",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "positionId",
"type": "uint256"
},
{
"indexed": false,
"internalType": "string",
"name": "name",
"type": "string"
},
{
"indexed": false,
"internalType": "string",
"name": "symbol",
"type": "string"
},
{
"indexed": false,
"internalType": "string",
"name": "metadataURI",
"type": "string"
}
],
"name": "TokenCreated",
"type": "event"
},
{
"inputs": [],
"name": "FEE_TIMELOCK",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "GRADUATION_WETH",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "MAX_CREATION_FEE",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "PLATFORM",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "PLATFORM_URL",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "TICK_SPACING",
"outputs": [
{
"internalType": "int24",
"name": "",
"type": "int24"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "TOTAL_SUPPLY",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "V3_FEE",
"outputs": [
{
"internalType": "uint24",
"name": "",
"type": "uint24"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "acceptOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"name": "allTokens",
"outputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "address",
"name": "pool",
"type": "address"
},
{
"internalType": "uint256",
"name": "positionId",
"type": "uint256"
},
{
"internalType": "address",
"name": "creator",
"type": "address"
},
{
"internalType": "string",
"name": "metadataURI",
"type": "string"
},
{
"internalType": "uint256",
"name": "createdAt",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "allTokensLength",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "applyFeeRecipient",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "cancelFeeRecipient",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "checkGraduation",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "string",
"name": "name",
"type": "string"
},
{
"internalType": "string",
"name": "symbol",
"type": "string"
},
{
"internalType": "string",
"name": "metadataURI",
"type": "string"
},
{
"internalType": "bytes32",
"name": "salt",
"type": "bytes32"
},
{
"internalType": "uint256",
"name": "minTokensOut",
"type": "uint256"
}
],
"name": "createToken",
"outputs": [
{
"internalType": "address",
"name": "tokenAddr",
"type": "address"
},
{
"internalType": "address",
"name": "pool",
"type": "address"
},
{
"internalType": "uint256",
"name": "positionId",
"type": "uint256"
}
],
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [],
"name": "creationFee",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "feeRecipient",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "feeRecipientEta",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "graduated",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "a",
"type": "address"
}
],
"name": "isCurve",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "locker",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "npm",
"outputs": [
{
"internalType": "contract INonfungiblePositionManager",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "owner",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "pendingFeeRecipient",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "pendingOwner",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "poolOf",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "creator",
"type": "address"
},
{
"internalType": "string",
"name": "name",
"type": "string"
},
{
"internalType": "string",
"name": "symbol",
"type": "string"
},
{
"internalType": "bytes32",
"name": "salt",
"type": "bytes32"
}
],
"name": "predictToken",
"outputs": [
{
"internalType": "address",
"name": "tokenAddr",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "r",
"type": "address"
}
],
"name": "proposeFeeRecipient",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "renounceOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "newFee",
"type": "uint256"
}
],
"name": "setCreationFee",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "sqrtPriceStartX96",
"outputs": [
{
"internalType": "uint160",
"name": "",
"type": "uint160"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "swapRouter",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "tickLower",
"outputs": [
{
"internalType": "int24",
"name": "",
"type": "int24"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "tickUpper",
"outputs": [
{
"internalType": "int24",
"name": "",
"type": "int24"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "transferOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"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 "@openzeppelin/contracts/access/Ownable2Step.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "./RawbinToken.sol";
import "./RawbinLocker.sol";
import "./interfaces/IUniswapV3.sol";
/// @dev Minimal Uniswap V3 SwapRouter02 subset for the optional same-tx dev buy.
interface ISwapRouter02Direct {
struct ExactInputSingleParams {
address tokenIn;
address tokenOut;
uint24 fee;
address recipient;
uint256 amountIn;
uint256 amountOutMinimum;
uint160 sqrtPriceLimitX96;
}
function exactInputSingle(ExactInputSingleParams calldata params) external payable returns (uint256 amountOut);
}
/// @title RawbinInstantDEX
/// @author Rawbin — the #1 launchpad on Robinhood Chain
/// @custom:website https://rawbin.fun
/// @custom:mission We're building a transparent launchpad. Verified contracts, no
/// hidden owner over tokens, no token tax, liquidity you can never rug. Read the
/// code and verify everything yourself. Every launch is a drop. — rawbin
/// @notice "Instant DEX" launch mode. One call deploys a RawbinToken and opens a real
/// Uniswap V3 pool immediately, seeding it *single-sided* (token only, no ETH)
/// over a fixed price range. Buyers walk the price up the range exactly like a
/// bonding curve, but it's a genuine DEX pool from block 0 — indexed by
/// DEXScreener/GMGN instantly, holder shown as "Uniswap V3" (not an insider).
/// The position NFT is locked in the Locker forever; the 1% pool fee is
/// collectable and split 75% platform / 25% creator.
///
/// Design notes:
/// - The meme token is always Uniswap `token1` (its address sorts ABOVE WETH),
/// enforced on-chain + by the off-chain grinder. This gives one code path and
/// lets us bake the price/tick constants (see script/compute-direct-params).
/// - No custom curve contract: the single-sided V3 position *is* the curve.
/// - This factory implements IRawbinFactoryView so it can reuse RawbinLocker
/// unchanged (it authorizes itself via isCurve(address(this)) == true).
contract RawbinInstantDEX is Ownable2Step, IRawbinFactoryView {
string public constant PLATFORM = "rawbin.fun";
string public constant PLATFORM_URL = "https://rawbin.fun";
uint256 public constant TOTAL_SUPPLY = 1_000_000_000 ether;
/// @notice The Rawbin mark, enforced on-chain: every token address ends in 0x…999.
uint160 internal constant VANITY_MASK = 0xFFF;
uint160 internal constant VANITY_SUFFIX = 0x999;
/// @notice Uniswap V3 pool fee tier (1%) and its tick spacing.
uint24 public constant V3_FEE = 10000;
int24 public constant TICK_SPACING = 200;
/// @notice Amount floor for the single-sided seed mint (basis points of the
/// token supply). At the exact start price the mint consumes ~100% of
/// TOTAL_SUPPLY; anything materially below that means the pool wasn't at
/// our price, so we revert instead of seeding a half-empty pool. This is
/// the explicit `amount1Min` an auditor expects and matches the floor the
/// bonding curve already enforces at migration (parity with RB-01).
uint256 internal constant MAX_SEED_SLIPPAGE_BPS = 200; // 2%
INonfungiblePositionManager public immutable npm;
address public immutable weth;
/// @notice Uniswap V3 SwapRouter02 — used for the optional same-tx dev buy.
address public immutable swapRouter;
/// @notice Dedicated Locker instance for direct launches (deployed + wired here).
address public immutable locker;
// Single-sided launch geometry — precomputed off-chain, fixed at deploy.
// Because the meme token is always token1 (addr > WETH), the position holds only
// token1 when the current tick is at/above tickUpper. So we initialize the pool at
// sqrtPriceStart == sqrtRatioAtTick(tickUpper) and mint the whole supply into
// [tickLower, tickUpper]; buys push the tick down through the range, selling token
// for WETH (price of the meme in WETH rises).
uint160 public immutable sqrtPriceStartX96;
int24 public immutable tickLower;
int24 public immutable tickUpper;
// Fee recipient — mutable behind a 24h timelock (propose -> apply), Ownable2Step.
address public override feeRecipient;
address public pendingFeeRecipient;
uint256 public feeRecipientEta;
uint256 public constant FEE_TIMELOCK = 24 hours;
uint256 public creationFee;
uint256 public constant MAX_CREATION_FEE = 0.005 ether;
struct TokenInfo {
address token;
address pool;
uint256 positionId;
address creator;
string metadataURI;
uint256 createdAt;
}
TokenInfo[] public allTokens;
/// @notice Graduation milestone: once the pool has collected this much WETH, the
/// token is flagged "graduated". Purely a status latch — it changes NOTHING
/// about the pool or the locked liquidity (no withdraw, no re-add); trading
/// continues exactly as before. It only powers the progress meter / status.
uint256 public constant GRADUATION_WETH = 10 ether;
mapping(address => address) public poolOf; // token => its V3 pool
mapping(address => bool) public graduated; // token => milestone reached (latched)
event Graduated(address indexed token, address indexed pool);
event TokenCreated(
address indexed token,
address indexed pool,
address indexed creator,
uint256 positionId,
string name,
string symbol,
string metadataURI
);
event CreationFeeUpdated(uint256 newFee);
event FeeRecipientProposed(address indexed next, uint256 eta);
event FeeRecipientUpdated(address indexed prev, address indexed next);
event FeeRecipientCancelled(address indexed cancelled);
constructor(
address feeRecipient_,
INonfungiblePositionManager npm_,
address swapRouter_,
uint160 sqrtPriceStartX96_,
int24 tickLower_,
int24 tickUpper_,
uint256 creationFee_
) Ownable(feeRecipient_) {
require(feeRecipient_ != address(0), "feeRecipient=0");
require(swapRouter_ != address(0), "router=0");
require(sqrtPriceStartX96_ > 0, "sqrtPrice=0");
require(tickLower_ < tickUpper_, "ticks");
require(tickLower_ % TICK_SPACING == 0 && tickUpper_ % TICK_SPACING == 0, "tick spacing");
require(creationFee_ <= MAX_CREATION_FEE, "fee > cap");
feeRecipient = feeRecipient_;
npm = npm_;
weth = npm_.WETH9();
swapRouter = swapRouter_;
sqrtPriceStartX96 = sqrtPriceStartX96_;
tickLower = tickLower_;
tickUpper = tickUpper_;
creationFee = creationFee_;
locker = address(new RawbinLocker(npm_, IRawbinFactoryView(address(this))));
}
// --------------------------------------------------- IRawbinFactoryView
// The Locker authorizes lockPosition() via isCurve(msg.sender). For direct
// launches the factory itself creates + locks the position, so it authorizes
// itself. It never marks any other address as a curve.
function isCurve(address a) external view override returns (bool) {
return a == address(this);
}
// feeRecipient() is the public state var getter (satisfies the interface).
// ------------------------------------------------------------------ admin
function setCreationFee(uint256 newFee) external onlyOwner {
require(newFee <= MAX_CREATION_FEE, "fee > cap");
creationFee = newFee;
emit CreationFeeUpdated(newFee);
}
function proposeFeeRecipient(address r) external onlyOwner {
require(r != address(0), "feeRecipient=0");
pendingFeeRecipient = r;
feeRecipientEta = block.timestamp + FEE_TIMELOCK;
emit FeeRecipientProposed(r, feeRecipientEta);
}
function applyFeeRecipient() external onlyOwner {
// forge-lint: disable-next-line(block-timestamp)
require(feeRecipientEta != 0 && block.timestamp >= feeRecipientEta, "timelock");
emit FeeRecipientUpdated(feeRecipient, pendingFeeRecipient);
feeRecipient = pendingFeeRecipient;
pendingFeeRecipient = address(0);
feeRecipientEta = 0;
}
function cancelFeeRecipient() external onlyOwner {
emit FeeRecipientCancelled(pendingFeeRecipient);
pendingFeeRecipient = address(0);
feeRecipientEta = 0;
}
// ----------------------------------------------------------------- create
/// @param salt Vanity salt ground off-chain so the token address ends in 0x…999
/// AND sorts above WETH (so the meme is token1). Namespaced by msg.sender.
/// @param minTokensOut Slippage bound for the optional same-tx dev buy — any ETH
/// above the creation fee buys tokens for the creator atomically at launch
/// (snipe-proof: the creator gets the first, cheapest tokens before bots).
function createToken(
string calldata name,
string calldata symbol,
string calldata metadataURI,
bytes32 salt,
uint256 minTokensOut
) external payable returns (address tokenAddr, address pool, uint256 positionId) {
require(msg.value >= creationFee, "creation fee not paid");
// Scope the token creation so `token` is freed before the rest (stack depth).
{
RawbinToken token =
new RawbinToken{salt: keccak256(abi.encodePacked(msg.sender, salt))}(name, symbol, TOTAL_SUPPLY, address(this));
tokenAddr = address(token);
// Enforce the Rawbin mark AND the token1 ordering (meme addr > WETH addr).
require(uint160(tokenAddr) & VANITY_MASK == VANITY_SUFFIX, "not a 0x...999 address");
require(uint160(tokenAddr) > uint160(weth), "token must sort above WETH");
token.initializeMetadata(metadataURI);
}
(pool, positionId) = _seedAndLock(tokenAddr, msg.sender);
_forwardFeeAndDevBuy(tokenAddr, msg.sender, minTokensOut);
_record(tokenAddr, pool, positionId, name, symbol, metadataURI);
}
/// Store + announce the launch (kept in its own frame to ease createToken's stack).
function _record(
address tokenAddr,
address pool,
uint256 positionId,
string calldata name,
string calldata symbol,
string calldata metadataURI
) internal {
poolOf[tokenAddr] = pool;
allTokens.push(TokenInfo({
token: tokenAddr,
pool: pool,
positionId: positionId,
creator: msg.sender,
metadataURI: metadataURI,
createdAt: block.timestamp
}));
emit TokenCreated(tokenAddr, pool, msg.sender, positionId, name, symbol, metadataURI);
}
/// @notice Permissionless graduation latch: flags the token graduated once its pool
/// holds >= GRADUATION_WETH. Read-only w.r.t. liquidity — safe for anyone to
/// call; it never touches the pool or the locked position.
function checkGraduation(address token) external returns (bool) {
if (graduated[token]) return true;
address p = poolOf[token];
require(p != address(0), "unknown token");
if (IERC20(weth).balanceOf(p) >= GRADUATION_WETH) {
graduated[token] = true;
emit Graduated(token, p);
return true;
}
return false;
}
/// Open the pool, seed it single-sided (all supply as token1, no WETH), lock the NFT.
function _seedAndLock(address tokenAddr, address creator) internal returns (address pool, uint256 positionId) {
pool = npm.createAndInitializePoolIfNecessary(weth, tokenAddr, V3_FEE, sqrtPriceStartX96);
// RB-02 / RB-06: the (WETH,token,1%) pool is deterministic and pre-createable.
// Unlike the curve (whose fair price is derived from the raise), our start
// price is a FIXED constant, so we can — and must — require EXACT equality.
// A band would let an attacker pre-init at +band above tickUpper, seeding a
// gapped pool that aggregators price above the liquidity (bad launch chart);
// and a price below tickUpper makes the single-sided mint compute 0 liquidity
// and revert anyway. Any off-price pool → revert; the creator re-grinds a
// fresh salt (=> a different, unpoisoned pool).
(uint160 poolSqrt, , , , , , ) = IUniswapV3Pool(pool).slot0();
require(poolSqrt == sqrtPriceStartX96, "pool price manipulated");
IERC20(tokenAddr).approve(address(npm), TOTAL_SUPPLY);
(positionId, , , ) = npm.mint(
INonfungiblePositionManager.MintParams({
token0: weth,
token1: tokenAddr,
fee: V3_FEE,
tickLower: tickLower,
tickUpper: tickUpper,
amount0Desired: 0,
amount1Desired: TOTAL_SUPPLY,
// amount0Min stays 0 on purpose: the seed is single-sided (token
// only, no WETH), so a nonzero WETH floor would always revert.
amount0Min: 0,
// Real floor on the token side: at least (100% - 2%) of supply must
// actually enter the pool, else the mint reverts (see RB-04 note).
amount1Min: TOTAL_SUPPLY - (TOTAL_SUPPLY * MAX_SEED_SLIPPAGE_BPS) / 10_000,
recipient: locker,
deadline: block.timestamp
})
);
// Lock it forever (Locker authorizes us via isCurve(address(this)) == true).
RawbinLocker(payable(locker)).lockPosition(positionId, creator);
// V3 single-sided mint leaves a few wei of dust (tick rounding). Sweep it to
// the creator so the factory never accumulates token balances.
uint256 dust = IERC20(tokenAddr).balanceOf(address(this));
if (dust > 0) require(IERC20(tokenAddr).transfer(creator, dust), "dust transfer failed");
}
/// Forward the creation fee; spend any excess ETH on an atomic dev buy for the
/// creator (snipe-proof first buy). No excess → nothing to do.
function _forwardFeeAndDevBuy(address tokenAddr, address buyer, uint256 minTokensOut) internal {
uint256 fee = creationFee;
if (fee > 0) {
(bool feeSent, ) = feeRecipient.call{value: fee}("");
require(feeSent, "fee transfer failed");
}
uint256 buyEth = msg.value - fee;
if (buyEth == 0) return;
// RB-09: a V3 exact-input swap can consume LESS than amountIn if the dev buy
// is large enough to exhaust the seeded range. The unspent WETH would then sit
// in the factory with no withdraw path. Measure the balance around the swap and
// refund any unspent WETH to the creator; reset the router allowance too.
uint256 wethBefore = IERC20(weth).balanceOf(address(this));
IWETH9(weth).deposit{value: buyEth}();
IERC20(weth).approve(swapRouter, buyEth);
ISwapRouter02Direct(swapRouter).exactInputSingle(
ISwapRouter02Direct.ExactInputSingleParams({
tokenIn: weth,
tokenOut: tokenAddr,
fee: V3_FEE,
recipient: buyer,
amountIn: buyEth,
amountOutMinimum: minTokensOut,
sqrtPriceLimitX96: 0
})
);
// Reset the allowance and refund any WETH the swap didn't consume.
IERC20(weth).approve(swapRouter, 0);
uint256 wethAfter = IERC20(weth).balanceOf(address(this));
if (wethAfter > wethBefore) {
require(IERC20(weth).transfer(buyer, wethAfter - wethBefore), "dev-buy refund failed");
}
}
function allTokensLength() external view returns (uint256) {
return allTokens.length;
}
/// @notice Predicts the CREATE2 token address for a creator + salt (grinder source
/// of truth). Supply is minted to this factory, so that's the ctor arg.
function predictToken(
address creator,
string calldata name,
string calldata symbol,
bytes32 salt
) external view returns (address tokenAddr) {
bytes32 nsSalt = keccak256(abi.encodePacked(creator, salt));
bytes32 initHash = keccak256(
abi.encodePacked(
type(RawbinToken).creationCode,
abi.encode(name, symbol, TOTAL_SUPPLY, address(this))
)
);
tokenAddr = address(uint160(uint256(
keccak256(abi.encodePacked(bytes1(0xff), address(this), nsSalt, initHash))
)));
}
}
Chain explorer2699msChain node88ms