BagsFactory
0x46ad6f53a3c26c8027826e2104cf0595b7b24d40
Verification
Verified
v0.8.26+commit.8a97fa7a
Type
Contract
6,233 bytes
ABI entries
53
18 read · 7 write
License
none
Contract information
- Address
- 0x46ad6f53a3c26c8027826e2104cf0595b7b24d40
- Chain
- Robinhood Chain (4663)
- Compiler
- v0.8.26+commit.8a97fa7a
- Optimization
- Enabled
- Creator
- 0xC6c66AeEbe…71616D6464
- Creation tx
- 0x5c291dcc91…5c2f3aa096
Token
Not a token
This contract does not expose ERC-20 metadata.
Read contract (18)
BONDING_CURVE_IMPL() → address
FEE_SHARE_IMPL() → address
HOOK() → address
PERMIT2() → address
POOL_MANAGER() → address
POSITION_MANAGER() → address
TOKEN_IMPL() → address
VAULT() → address
WETH() → address
allTokens(uint256) → address
allTokensLength() → uint256
creationFee() → uint256
curveForToken(address) → address
feeShareForToken(address) → address
getTokens(uint256, uint256) → address[]
graduationThreshold() → uint256
owner() → address
tokenForPoolId(bytes32) → address
Events (5)
CreationFeeUpdatedGraduationThresholdUpdatedOwnershipTransferredRescuedTokenCreated
ABI
[
{
"inputs": [
{
"internalType": "address",
"name": "poolManager",
"type": "address"
},
{
"internalType": "address",
"name": "positionManager",
"type": "address"
},
{
"internalType": "address",
"name": "weth",
"type": "address"
},
{
"internalType": "address",
"name": "vault",
"type": "address"
},
{
"internalType": "address",
"name": "permit2",
"type": "address"
},
{
"internalType": "address",
"name": "hook",
"type": "address"
},
{
"internalType": "address",
"name": "tokenImpl",
"type": "address"
},
{
"internalType": "address",
"name": "feeShareImpl",
"type": "address"
},
{
"internalType": "address",
"name": "bondingCurveImpl",
"type": "address"
},
{
"internalType": "uint256",
"name": "graduationThreshold_",
"type": "uint256"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"inputs": [
{
"internalType": "address",
"name": "curve",
"type": "address"
},
{
"internalType": "uint256",
"name": "value",
"type": "uint256"
}
],
"name": "BagsFactory_BuyFailed",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "BagsFactory_FeeTransferFailed",
"type": "error"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "required",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "sent",
"type": "uint256"
}
],
"name": "BagsFactory_InsufficientCreationFee",
"type": "error"
},
{
"inputs": [],
"name": "BagsFactory_InvalidClaimers",
"type": "error"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "threshold",
"type": "uint256"
}
],
"name": "BagsFactory_InvalidGraduationThreshold",
"type": "error"
},
{
"inputs": [
{
"internalType": "uint16",
"name": "partnerBps",
"type": "uint16"
}
],
"name": "BagsFactory_InvalidPartnerBps",
"type": "error"
},
{
"inputs": [],
"name": "BagsFactory_NoBuyValue",
"type": "error"
},
{
"inputs": [],
"name": "BagsFactory_NoClaimers",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "BagsFactory_RefundFailed",
"type": "error"
},
{
"inputs": [],
"name": "BagsFactory_ZeroAddressHook",
"type": "error"
},
{
"inputs": [],
"name": "BagsFactory_ZeroAddressImpl",
"type": "error"
},
{
"inputs": [],
"name": "BagsFactory_ZeroAddressPermit2",
"type": "error"
},
{
"inputs": [],
"name": "BagsFactory_ZeroAddressPoolManager",
"type": "error"
},
{
"inputs": [],
"name": "BagsFactory_ZeroAddressPositionManager",
"type": "error"
},
{
"inputs": [],
"name": "BagsFactory_ZeroAddressVault",
"type": "error"
},
{
"inputs": [],
"name": "BagsFactory_ZeroAddressWETH",
"type": "error"
},
{
"inputs": [],
"name": "FailedDeployment",
"type": "error"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "balance",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "needed",
"type": "uint256"
}
],
"name": "InsufficientBalance",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "owner",
"type": "address"
}
],
"name": "OwnableInvalidOwner",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "account",
"type": "address"
}
],
"name": "OwnableUnauthorizedAccount",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "SafeERC20FailedOperation",
"type": "error"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "uint256",
"name": "newFee",
"type": "uint256"
}
],
"name": "CreationFeeUpdated",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "uint256",
"name": "oldThreshold",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "newThreshold",
"type": "uint256"
}
],
"name": "GraduationThresholdUpdated",
"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": "to",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "Rescued",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "curve",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "creator",
"type": "address"
},
{
"indexed": false,
"internalType": "address",
"name": "feeShare",
"type": "address"
},
{
"indexed": false,
"internalType": "PoolId",
"name": "poolId",
"type": "bytes32"
},
{
"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": "BONDING_CURVE_IMPL",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "FEE_SHARE_IMPL",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "HOOK",
"outputs": [
{
"internalType": "contract BagsV4Hook",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "PERMIT2",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "POOL_MANAGER",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "POSITION_MANAGER",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "TOKEN_IMPL",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "VAULT",
"outputs": [
{
"internalType": "contract BagsVault",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "WETH",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"name": "allTokens",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "allTokensLength",
"outputs": [
{
"internalType": "uint256",
"name": "length",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "string",
"name": "name",
"type": "string"
},
{
"internalType": "string",
"name": "symbol",
"type": "string"
},
{
"internalType": "string",
"name": "metadataURI",
"type": "string"
},
{
"internalType": "address",
"name": "partner",
"type": "address"
},
{
"internalType": "uint16",
"name": "partnerBps",
"type": "uint16"
},
{
"internalType": "address[]",
"name": "claimers",
"type": "address[]"
},
{
"internalType": "uint16[]",
"name": "bps",
"type": "uint16[]"
}
],
"name": "create",
"outputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "address",
"name": "curve",
"type": "address"
}
],
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [
{
"internalType": "string",
"name": "name",
"type": "string"
},
{
"internalType": "string",
"name": "symbol",
"type": "string"
},
{
"internalType": "string",
"name": "metadataURI",
"type": "string"
},
{
"internalType": "address",
"name": "partner",
"type": "address"
},
{
"internalType": "uint16",
"name": "partnerBps",
"type": "uint16"
},
{
"internalType": "address[]",
"name": "claimers",
"type": "address[]"
},
{
"internalType": "uint16[]",
"name": "bps",
"type": "uint16[]"
}
],
"name": "createAndBuy",
"outputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "address",
"name": "curve",
"type": "address"
}
],
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [],
"name": "creationFee",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "curveForToken",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "feeShareForToken",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "offset",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "limit",
"type": "uint256"
}
],
"name": "getTokens",
"outputs": [
{
"internalType": "address[]",
"name": "tokens",
"type": "address[]"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "graduationThreshold",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "owner",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "renounceOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "rescue",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "newFee",
"type": "uint256"
}
],
"name": "setCreationFee",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "newThreshold",
"type": "uint256"
}
],
"name": "setGraduationThreshold",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "PoolId",
"name": "",
"type": "bytes32"
}
],
"name": "tokenForPoolId",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "transferOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"stateMutability": "payable",
"type": "receive"
}
]Source code
// SPDX-License-Identifier: Unlicense
pragma solidity 0.8.26;
// OpenZeppelin
import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
// Uniswap v4 Core
import {Currency} from "@uniswap/v4-core/src/types/Currency.sol";
import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol";
import {LPFeeLibrary} from "@uniswap/v4-core/src/libraries/LPFeeLibrary.sol";
import {PoolId} from "@uniswap/v4-core/src/types/PoolId.sol";
import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
// Local contracts (used for type-casting clones, NOT for `new` deployment)
import {BagsBondingCurve} from "./BagsBondingCurve.sol";
import {BagsFeeShare} from "./BagsFeeShare.sol";
import {BagsToken} from "./BagsToken.sol";
import {BagsV4Hook} from "./BagsV4Hook.sol";
import {BagsVault} from "./BagsVault.sol";
// Local interfaces
import {IBagsFactory} from "src/interfaces/IBagsFactory.sol";
/// @title BagsFactory
/// @notice Deploys `BagsToken` + `BagsBondingCurve` pairs using EIP-1167 minimal proxy clones.
/// Implementation contracts are deployed once; the factory clones them cheaply per token launch.
/// All launches share one `BagsV4Hook` instance; the factory registers each pool with it.
/// @author Bags
contract BagsFactory is Ownable, IBagsFactory {
using SafeERC20 for IERC20;
/// @notice Basis points denominator (10_000 = 100%)
uint256 private constant BPS_DENOM = 10_000;
/// @notice Maximum number of claimers supported per token
uint256 private constant MAX_CLAIMERS = 100;
/// @notice Default creation fee for launching a token
uint256 private constant DEFAULT_CREATION_FEE = 0.02 ether;
/// @notice Default minimum tokens out used for the initial buy in createAndBuy
/// @dev Atomic deployment+buy makes front-run slippage unlikely; keep minimal protection.
uint256 private constant INITIAL_BUY_MIN_TOKENS_OUT = 1;
/// @notice Minimum allowed graduation threshold
uint256 private constant MIN_GRADUATION_THRESHOLD = 0.01 ether;
/// @notice Maximum allowed graduation threshold
uint256 private constant MAX_GRADUATION_THRESHOLD = 1000 ether;
/// @notice Tick spacing of every Bags pool (must byte-match the curve's `_migrate` key)
int24 private constant TICK_SPACING = 60;
/// @notice Uniswap v4 PoolManager (set at deployment)
address public immutable POOL_MANAGER;
/// @notice Uniswap v4 PositionManager (set at deployment)
address public immutable POSITION_MANAGER;
/// @notice WETH address for the target chain
address public immutable WETH;
/// @notice Permit2 contract address
address public immutable PERMIT2;
/// @notice Global vault used by all bonding curves for fee collection
BagsVault public immutable VAULT;
/// @notice Shared Bags v4 hook instance attached to every pool
BagsV4Hook public immutable HOOK;
// --- Implementation addresses for EIP-1167 cloning ---
/// @notice BagsToken implementation (clone template)
address public immutable TOKEN_IMPL;
/// @notice BagsFeeShare implementation (clone template)
address public immutable FEE_SHARE_IMPL;
/// @notice BagsBondingCurve implementation (clone template)
address public immutable BONDING_CURVE_IMPL;
/// @notice Creation fee required to create a new pair
uint256 public creationFee = DEFAULT_CREATION_FEE;
/// @notice Graduation threshold snapshotted into each new launch (owner-settable)
/// @dev Changing it never affects live curves: each clone stores the value at initialize().
/// Any value within the bounds satisfies the curve's economics asserts (verified for the
/// full [0.01 ether, 1000 ether] range).
uint256 public graduationThreshold;
// --- On-chain registry (integrator/indexer surface) ---
/// @notice Bonding curve for a launched token
mapping(address => address) public curveForToken;
/// @notice FeeShare contract for a launched token
mapping(address => address) public feeShareForToken;
/// @notice Reverse lookup from v4 pool id to the launched token
mapping(PoolId => address) public tokenForPoolId;
/// @notice All tokens ever launched by this factory
address[] public allTokens;
/// @notice Creates the factory with chain-specific infrastructure addresses and implementation contracts
/// @param poolManager Uniswap v4 PoolManager address
/// @param positionManager Uniswap v4 PositionManager address
/// @param weth WETH address for the target chain
/// @param vault Global Bags vault address
/// @param permit2 Permit2 contract address
/// @param hook Shared BagsV4Hook instance address
/// @param tokenImpl BagsToken implementation address
/// @param feeShareImpl BagsFeeShare implementation address
/// @param bondingCurveImpl BagsBondingCurve implementation address
/// @param graduationThreshold_ Initial graduation threshold (net quote raise per launch)
constructor(
address poolManager,
address positionManager,
address weth,
address vault,
address permit2,
address hook,
address tokenImpl,
address feeShareImpl,
address bondingCurveImpl,
uint256 graduationThreshold_
) Ownable(msg.sender) {
if (poolManager == address(0)) revert BagsFactory_ZeroAddressPoolManager();
if (positionManager == address(0)) revert BagsFactory_ZeroAddressPositionManager();
if (weth == address(0)) revert BagsFactory_ZeroAddressWETH();
if (vault == address(0)) revert BagsFactory_ZeroAddressVault();
if (permit2 == address(0)) revert BagsFactory_ZeroAddressPermit2();
if (hook == address(0)) revert BagsFactory_ZeroAddressHook();
if (tokenImpl == address(0)) revert BagsFactory_ZeroAddressImpl();
if (feeShareImpl == address(0)) revert BagsFactory_ZeroAddressImpl();
if (bondingCurveImpl == address(0)) revert BagsFactory_ZeroAddressImpl();
POOL_MANAGER = poolManager;
POSITION_MANAGER = positionManager;
WETH = weth;
PERMIT2 = permit2;
VAULT = BagsVault(payable(vault));
HOOK = BagsV4Hook(payable(hook));
TOKEN_IMPL = tokenImpl;
FEE_SHARE_IMPL = feeShareImpl;
BONDING_CURVE_IMPL = bondingCurveImpl;
_setGraduationThreshold(graduationThreshold_);
}
/// @notice Accept native transfers (needed to receive curve refunds during createAndBuy)
receive() external payable {}
// ====================================================================
// EXTERNAL FUNCTIONS
// ====================================================================
/// @notice Update the creation fee required to create a new pair
/// @param newFee New creation fee in native quote
function setCreationFee(
uint256 newFee
) external override onlyOwner {
creationFee = newFee;
emit CreationFeeUpdated(newFee);
}
/// @notice Update the graduation threshold snapshotted into future launches
/// @dev Bounds-checked; live curves keep the value they snapshotted at creation.
/// @param newThreshold New graduation threshold in quote wei
function setGraduationThreshold(
uint256 newThreshold
) external override onlyOwner {
_setGraduationThreshold(newThreshold);
}
/// @notice Number of tokens ever launched by this factory
/// @return length Length of the allTokens array
function allTokensLength() external view override returns (uint256 length) {
return allTokens.length;
}
/// @notice Paged getter over all launched tokens
/// @param offset Index of the first token to return
/// @param limit Maximum number of tokens to return
/// @return tokens Token addresses in launch order
function getTokens(
uint256 offset,
uint256 limit
) external view override returns (address[] memory tokens) {
uint256 total = allTokens.length;
if (offset >= total) return new address[](0);
uint256 end = offset + limit;
if (end > total) end = total;
tokens = new address[](end - offset);
for (uint256 i = offset; i < end; ++i) {
tokens[i - offset] = allTokens[i];
}
}
/// @notice Create a new token + bonding curve pair
/// @param name Token name
/// @param symbol Token symbol
/// @param metadataURI Off-chain metadata URI for token
/// @param partner Optional partner address for fee sharing
/// @param partnerBps Partner share in bps (base 10_000) applied to creator fees
/// @param claimers List of claimer addresses for creator fee distribution
/// @param bps BPS shares for each claimer (must sum to 10_000)
/// @return token Newly deployed token address
/// @return curve Newly deployed bonding curve address
/// @dev The creator is set to msg.sender. With the FeeShare admin share removed, the claimers
/// array is the only creator-side fee routing: include the creator address for them to earn.
function create(
string calldata name,
string calldata symbol,
string calldata metadataURI,
address partner,
uint16 partnerBps,
address[] calldata claimers,
uint16[] calldata bps
) external payable override returns (address token, address curve) {
if (msg.value < creationFee) {
revert BagsFactory_InsufficientCreationFee(creationFee, msg.value);
}
(BagsToken deployedToken, BagsBondingCurve deployedCurve) =
_deployPair(name, symbol, metadataURI, msg.sender, partner, partnerBps, claimers, bps);
// Send only the creation fee to the vault, refund any surplus to sender
(bool okFee,) = payable(address(VAULT)).call{value: creationFee}("");
if (!okFee) revert BagsFactory_FeeTransferFailed(address(VAULT), creationFee);
uint256 refund = msg.value - creationFee;
if (refund > 0) {
(bool okRefund,) = msg.sender.call{value: refund}("");
if (!okRefund) revert BagsFactory_RefundFailed(msg.sender, refund);
}
return (address(deployedToken), address(deployedCurve));
}
/// @notice Create a new token+curve pair and perform an initial buy on behalf of the creator
/// @param name Token name
/// @param symbol Token symbol
/// @param metadataURI Off-chain metadata URI for token
/// @param partner Optional partner address for fee sharing
/// @param partnerBps Partner share in bps (base 10_000) applied to creator fees
/// @param claimers List of claimer addresses for creator fee distribution
/// @param bps BPS shares for each claimer (must sum to 10_000)
/// @return token Newly deployed token address
/// @return curve Newly deployed bonding curve address
/// @dev Sends exactly `creationFee` to vault, uses remaining value for the buy, forwards tokens/refund to sender
function createAndBuy(
string calldata name,
string calldata symbol,
string calldata metadataURI,
address partner,
uint16 partnerBps,
address[] calldata claimers,
uint16[] calldata bps
) external payable override returns (address token, address curve) {
if (msg.value < creationFee) {
revert BagsFactory_InsufficientCreationFee(creationFee, msg.value);
}
uint256 buyValue = msg.value - creationFee;
if (buyValue == 0) revert BagsFactory_NoBuyValue();
// Record baseline balance to compute exact refund after buy
uint256 baseline = address(this).balance - msg.value;
(BagsToken deployedToken, BagsBondingCurve deployedCurve) =
_deployPair(name, symbol, metadataURI, msg.sender, partner, partnerBps, claimers, bps);
// Pay creation fee to the vault
(bool okFee,) = payable(address(VAULT)).call{value: creationFee}("");
if (!okFee) revert BagsFactory_FeeTransferFailed(address(VAULT), creationFee);
// Perform initial buy on behalf of the creator.
(bool okBuy,) = address(deployedCurve).call{value: buyValue}(
abi.encodeWithSignature("buyFor(address,uint256)", msg.sender, INITIAL_BUY_MIN_TOKENS_OUT)
);
if (!okBuy) revert BagsFactory_BuyFailed(address(deployedCurve), buyValue);
// Any refund from the curve buy has already been returned to the factory.
// Forward any refund to the creator (if curve limited the gross)
uint256 refund = address(this).balance - baseline;
if (refund > 0) {
(bool okRefund,) = msg.sender.call{value: refund}("");
if (!okRefund) revert BagsFactory_RefundFailed(msg.sender, refund);
}
return (address(deployedToken), address(deployedCurve));
}
/// @notice Withdraw mistakenly sent funds
/// @param token Token address to rescue (zero for native)
/// @param amount Amount to rescue
function rescue(
address token,
uint256 amount
) external override onlyOwner {
if (token == address(0)) {
(bool ok,) = owner().call{value: amount}("");
if (!ok) revert BagsFactory_FeeTransferFailed(owner(), amount);
emit Rescued(address(0), owner(), amount);
return;
}
IERC20(token).safeTransfer(owner(), amount);
emit Rescued(token, owner(), amount);
}
// ====================================================================
// INTERNAL FUNCTIONS
// ====================================================================
/// @dev Bounds-checks and stores the graduation threshold, emitting {GraduationThresholdUpdated}
function _setGraduationThreshold(
uint256 newThreshold
) internal {
if (newThreshold < MIN_GRADUATION_THRESHOLD || newThreshold > MAX_GRADUATION_THRESHOLD) {
revert BagsFactory_InvalidGraduationThreshold(newThreshold);
}
uint256 oldThreshold = graduationThreshold;
graduationThreshold = newThreshold;
emit GraduationThresholdUpdated(oldThreshold, newThreshold);
}
/// @dev Builds the canonical PoolKey for a launched token. Must byte-match the key the curve
/// builds in `_migrate` (sorted token/WETH, dynamic fee flag, tick spacing 60, shared hook).
/// @param token Launched token address
/// @return key Canonical pool key for the token's migration pool
function _poolKeyFor(
address token
) internal view returns (PoolKey memory key) {
bool tokenIs0 = token < WETH;
key = PoolKey({
currency0: Currency.wrap(tokenIs0 ? token : WETH),
currency1: Currency.wrap(tokenIs0 ? WETH : token),
fee: LPFeeLibrary.DYNAMIC_FEE_FLAG,
tickSpacing: TICK_SPACING,
hooks: IHooks(address(HOOK))
});
}
/// @dev Internal helper to deploy token and curve via EIP-1167 clones, mint, initialize,
/// register the pool with the shared hook, and populate the registry
function _deployPair(
string calldata name,
string calldata symbol,
string calldata metadataURI,
address creator,
address partner,
uint16 partnerBps,
address[] memory claimers,
uint16[] memory bps
) internal returns (BagsToken token, BagsBondingCurve curve) {
// Validate partnerBps
if (partnerBps > BPS_DENOM) revert BagsFactory_InvalidPartnerBps(partnerBps);
// Validate claimers config
uint256 claimersLength = claimers.length;
if (claimersLength == 0) revert BagsFactory_NoClaimers();
if (claimersLength != bps.length || claimersLength > MAX_CLAIMERS) revert BagsFactory_InvalidClaimers();
uint256 sum;
for (uint256 i; i < claimersLength; ++i) {
if (claimers[i] == address(0) || claimers[i] == partner) revert BagsFactory_InvalidClaimers();
for (uint256 j = i + 1; j < claimersLength; ++j) {
if (claimers[i] == claimers[j]) revert BagsFactory_InvalidClaimers();
}
sum += bps[i];
}
if (sum != BPS_DENOM) revert BagsFactory_InvalidClaimers();
// Clone and initialize BagsToken
token = BagsToken(Clones.clone(TOKEN_IMPL));
token.initialize(name, symbol, metadataURI, address(this));
// The pool id is deterministic as soon as the token address is known
PoolId poolId = _poolKeyFor(address(token)).toId();
// Clone and initialize BagsFeeShare, wired to the shared hook. The factory is the
// temporary admin so it can call setBondingCurve; ownership moves to the platform below.
BagsFeeShare feeShare = BagsFeeShare(payable(Clones.clone(FEE_SHARE_IMPL)));
feeShare.initialize(WETH, address(this), partner, partnerBps, claimers, bps, address(HOOK), poolId);
// Clone and initialize BagsBondingCurve. The factory stays owner only for
// initializeAfterMint; pause authority and the LP NFT go to the factory owner.
curve = BagsBondingCurve(payable(Clones.clone(BONDING_CURVE_IMPL)));
curve.initialize(
address(token), address(HOOK), address(this), creator, address(feeShare), owner(), graduationThreshold
);
// Wire bonding curve into feeshare for pre-migration fee notifications,
// then hand the fee share admin over to the platform (factory owner)
feeShare.setBondingCurve(address(curve));
feeShare.transferOwnership(owner());
// Register the pool with the shared hook: authorizes the curve to initialize the pool
// and routes the creator half of swept fees to the fee share
HOOK.register(poolId, address(curve), address(feeShare));
// Mint full supply to curve
token.mintInitialSupply(address(curve));
// Sync curve real token reserves
curve.initializeAfterMint();
// Transfer token ownership to curve so it can renounce later; or dead directly
token.transferOwnership(address(curve));
// Populate the on-chain registry
curveForToken[address(token)] = address(curve);
feeShareForToken[address(token)] = address(feeShare);
tokenForPoolId[poolId] = address(token);
allTokens.push(address(token));
emit TokenCreated(
address(token), address(curve), creator, address(feeShare), poolId, name, symbol, metadataURI
);
}
}
Chain explorer1932msChain node76ms