BangTokenDeployer

0x2c0e1145234a69749fee778b96c76d5f4d2f1325

Verification
Verified
v0.8.26+commit.8a97fa7a
Type
Contract
3,612 bytes
ABI entries
3
1 read · 2 write
License
none

Contract information

Address
0x2c0e1145234a69749fee778b96c76d5f4d2f1325
Chain
Robinhood Chain (4663)
Compiler
v0.8.26+commit.8a97fa7a
Optimization
Enabled
Creation tx
0x88b2500717…0f592b2a5c

Token

Not a token

This contract does not expose ERC-20 metadata.

Read contract (1)

factory()address

ABI

[
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "_factory",
        "type": "address"
      }
    ],
    "name": "bindFactory",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "string",
        "name": "name_",
        "type": "string"
      },
      {
        "internalType": "string",
        "name": "symbol_",
        "type": "string"
      },
      {
        "internalType": "address",
        "name": "mintTo",
        "type": "address"
      }
    ],
    "name": "deploy",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "factory",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  }
]

Source code

// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;

import {IPoolManager} from "v4-core/src/interfaces/IPoolManager.sol";
import {IUnlockCallback} from "v4-core/src/interfaces/callback/IUnlockCallback.sol";
import {PoolKey} from "v4-core/src/types/PoolKey.sol";
import {PoolId, PoolIdLibrary} from "v4-core/src/types/PoolId.sol";
import {Currency} from "v4-core/src/types/Currency.sol";
import {IHooks} from "v4-core/src/interfaces/IHooks.sol";
import {ModifyLiquidityParams} from "v4-core/src/types/PoolOperation.sol";
import {BalanceDelta} from "v4-core/src/types/BalanceDelta.sol";
import {TickMath} from "v4-core/src/libraries/TickMath.sol";
import {LiquidityAmounts} from "v4-periphery/src/libraries/LiquidityAmounts.sol";
import {BangToken} from "./BangToken.sol";
import {BangDividendVault} from "./BangDividendVault.sol";

interface IBangHookRegister {
    function registerPool(PoolKey calldata key, address vault, uint16 creatorRateBps) external;
}

interface IVaultDeployer {
    function deploy(
        address token, address hook, address universalRouter, address weth, address usdg,
        address[] calldata rewardTokens, uint16[] calldata weightsBps, bytes[] calldata routes
    ) external returns (address);
}

interface ITokenDeployer {
    function deploy(string calldata name, string calldata symbol, address mintTo) external returns (address);
}

/**
 * ================================================================
 * BangFactory — one transaction, one ownerless dividend machine
 * ================================================================
 * Launch flow, atomic:
 *   1. deploy the plain ERC-20 (full supply to this factory)
 *   2. deploy the dividend vault with the creator's stock basket
 *   3. wire token -> vault (one-shot, never again)
 *   4. register the pool config with the shared BangHook
 *   5. initialize the V4 pool at the creator's chosen price
 *      -> starting market cap is set by PRICE, not by capital.
 *         This is why a $2K launch costs cents instead of 1 ETH.
 *   6. add ALL supply as single-sided liquidity in a range below spot
 *   7. burn any remainder
 *
 * The LP position is held by this factory as a raw PoolManager position
 * and NO function exists to remove it. That is a stronger lock than
 * minting an NFT and burning it — there is no token to lose, transfer,
 * or mis-send, and no code path that can decrease liquidity.
 *
 * Ownerless launches: the token has no owner, the vault has no owner and
 * no withdrawal path, and the hook's platform wallet + 1% are immutable.
 * ================================================================
 */
contract BangFactory is IUnlockCallback {
    using PoolIdLibrary for PoolKey;

    address public owner;

    IPoolManager public immutable poolManager;
    address public immutable hook;
    address public immutable universalRouter;
    address public immutable weth;
    address public immutable usdg;
    ITokenDeployer public immutable tokenDeployer;
    IVaultDeployer public immutable vaultDeployer;
    address payable public feeRecipient;

    uint256 public creationFee = 0.0005 ether;
    uint256 public constant MAX_CREATION_FEE = 0.05 ether; // creators can't be gouged
    uint16 public constant MIN_CREATOR_BPS = 100;  // 1%
    uint16 public constant MAX_CREATOR_BPS = 500;  // 5%
    uint24 public constant POOL_FEE = 0;           // all economics live in the hook
    int24 public constant TICK_SPACING = 60;

    struct StockRoute {
        bytes route;   // pre-built Universal Router calldata: ETH -> USDG -> stock
        bool listed;
    }
    mapping(address => StockRoute) public stocks;
    address[] public stockList;

    struct Launch {
        address token;
        address vault;
        address creator;
        uint16 creatorRateBps;
        uint256 createdAt;
    }
    Launch[] public launches;
    mapping(string => address) public tickerToToken;

    event StockListed(address indexed stock);
    event BangLaunched(
        address indexed token, address indexed creator, address vault,
        uint16 creatorRateBps, uint160 sqrtPriceX96, int24 tickLower, int24 tickUpper,
        address[] basket, uint16[] weights
    );

    error NotOwner();
    error TickerTaken();
    error BadRate();
    error BadFee();
    error NotWhitelisted();
    error BadBasket();
    error NotPoolManager();
    error BadRange();

    modifier onlyOwner() {
        if (msg.sender != owner) revert NotOwner();
        _;
    }

    constructor(
        IPoolManager _poolManager,
        address _hook,
        address _universalRouter,
        address _weth,
        address _usdg,
        address payable _feeRecipient,
        address _tokenDeployer,
        address _vaultDeployer
    ) {
        owner = msg.sender;
        poolManager = _poolManager;
        hook = _hook;
        universalRouter = _universalRouter;
        weth = _weth;
        usdg = _usdg;
        feeRecipient = _feeRecipient;
        tokenDeployer = ITokenDeployer(_tokenDeployer);
        vaultDeployer = IVaultDeployer(_vaultDeployer);
    }

    receive() external payable {}

    // ---------------------------------------------------------------
    // THE LAUNCH
    // ---------------------------------------------------------------
    /**
     * @param sqrtPriceX96 starting price -> sets market cap with no capital
     * @param tickLower    bottom of the single-sided range (below spot)
     * @param creatorRateBps dividend tax the creator picks, 100-500 (1-5%).
     *        Traders pay this PLUS the immutable 1% launchpad fee.
     */
    function launch(
        string calldata name_,
        string calldata symbol_,
        uint16 creatorRateBps,
        uint160 sqrtPriceX96,
        int24 tickLower,
        address[] calldata basket,
        uint16[] calldata weights
    ) external payable returns (address tokenAddr) {
        if (msg.value < creationFee) revert BadFee();
        if (creatorRateBps < MIN_CREATOR_BPS || creatorRateBps > MAX_CREATOR_BPS) revert BadRate();
        if (tickerToToken[symbol_] != address(0)) revert TickerTaken();
        if (bytes(symbol_).length < 2 || bytes(symbol_).length > 10) revert TickerTaken();

        bytes[] memory routes = _validateBasket(basket, weights);

        // 1-3. token + vault + wiring
        address token = tokenDeployer.deploy(name_, symbol_, address(this));
        address vault = vaultDeployer.deploy(
            token, hook, universalRouter, weth, usdg, basket, weights, routes
        );
        BangToken(token).wireVault(vault);

        // 4-6. pool: register config, initialize at price, lock liquidity
        PoolKey memory key = PoolKey({
            currency0: Currency.wrap(address(0)),      // native ETH
            currency1: Currency.wrap(token),
            fee: POOL_FEE,
            tickSpacing: TICK_SPACING,
            hooks: IHooks(hook)
        });

        IBangHookRegister(hook).registerPool(key, vault, creatorRateBps);
        BangDividendVault(payable(vault)).setPoolKey(key, address(poolManager));

        int24 tick = poolManager.initialize(key, sqrtPriceX96);
        int24 tickUpper = _alignDown(tick);
        if (tickLower >= tickUpper) revert BadRange();
        tickLower = _alignDown(tickLower);

        poolManager.unlock(abi.encode(key, tickLower, tickUpper, token));

        // 7. burn any dust the pool didn't take
        uint256 rem = BangToken(token).balanceOf(address(this));
        if (rem > 0) {
            BangToken(token).transfer(0x000000000000000000000000000000000000dEaD, rem);
        }

        // fee out
        (bool ok,) = feeRecipient.call{value: creationFee}("");
        if (!ok) revert BadFee();

        tickerToToken[symbol_] = token;
        launches.push(Launch(token, vault, msg.sender, creatorRateBps, block.timestamp));
        emit BangLaunched(
            token, msg.sender, vault, creatorRateBps,
            sqrtPriceX96, tickLower, tickUpper, basket, weights
        );
        return token;
    }

    /// Adds the entire supply as single-sided token liquidity. The position
    /// belongs to this factory forever — no code path can ever remove it.
    function unlockCallback(bytes calldata data) external returns (bytes memory) {
        if (msg.sender != address(poolManager)) revert NotPoolManager();
        (PoolKey memory key, int24 tickLower, int24 tickUpper, address token) =
            abi.decode(data, (PoolKey, int24, int24, address));

        uint256 amount1 = BangToken(token).balanceOf(address(this));

        uint128 liquidity = LiquidityAmounts.getLiquidityForAmount1(
            TickMath.getSqrtPriceAtTick(tickLower),
            TickMath.getSqrtPriceAtTick(tickUpper),
            amount1
        );

        (BalanceDelta delta,) = poolManager.modifyLiquidity(
            key,
            ModifyLiquidityParams({
                tickLower: tickLower,
                tickUpper: tickUpper,
                liquidityDelta: int256(uint256(liquidity)),
                salt: bytes32(0)
            }),
            ""
        );

        // Settle what we owe the pool in BANG tokens.
        int128 owed1 = delta.amount1();
        if (owed1 < 0) {
            uint256 amountOwed = uint256(uint128(-owed1));
            poolManager.sync(key.currency1);
            BangToken(token).transfer(address(poolManager), amountOwed);
            poolManager.settle();
        }
        return "";
    }

    function _validateBasket(address[] calldata basket, uint16[] calldata weights)
        internal view returns (bytes[] memory routes)
    {
        uint256 n = basket.length;
        if (n == 0 || n > 5 || weights.length != n) revert BadBasket();
        routes = new bytes[](n);
        uint256 sum;
        for (uint256 i = 0; i < n; i++) {
            StockRoute storage r = stocks[basket[i]];
            if (!r.listed) revert NotWhitelisted();
            if (weights[i] == 0) revert BadBasket();
            for (uint256 j = 0; j < i; j++) {
                if (basket[j] == basket[i]) revert BadBasket();
            }
            sum += weights[i];
            routes[i] = r.route;
        }
        if (sum != 10_000) revert BadBasket();
    }

    function _alignDown(int24 tick) internal pure returns (int24) {
        int24 compressed = tick / TICK_SPACING;
        if (tick < 0 && tick % TICK_SPACING != 0) compressed--;
        return compressed * TICK_SPACING;
    }

    // ---------------------------------------------------------------
    // Views
    // ---------------------------------------------------------------
    function launchCount() external view returns (uint256) { return launches.length; }
    function stockCount() external view returns (uint256) { return stockList.length; }

    function getLaunches(uint256 start, uint256 max) external view returns (Launch[] memory out) {
        uint256 n = launches.length;
        if (start >= n) return new Launch[](0);
        uint256 end = start + max > n ? n : start + max;
        out = new Launch[](end - start);
        for (uint256 i = start; i < end; i++) out[i - start] = launches[i];
    }

    // ---------------------------------------------------------------
    // Curated admin — whitelist is ADD-ONLY; cannot touch live launches
    // ---------------------------------------------------------------
    function listStock(address stock, bytes calldata route) external onlyOwner {
        if (stock == address(0) || route.length == 0) revert NotWhitelisted();
        if (stocks[stock].listed) revert NotWhitelisted();
        stocks[stock] = StockRoute(route, true);
        stockList.push(stock);
        emit StockListed(stock);
    }

    function setFees(uint256 _creationFee, address payable _recipient) external onlyOwner {
        if (_creationFee > MAX_CREATION_FEE || _recipient == address(0)) revert BadFee();
        creationFee = _creationFee;
        feeRecipient = _recipient;
    }

    function transferOwnership(address newOwner) external onlyOwner {
        if (newOwner == address(0)) revert NotOwner();
        owner = newOwner;
    }
}

// ================================================================
// Deployers — keep the factory under the 24KB EIP-170 limit
// ================================================================
abstract contract FactoryBound {
    address public factory;

    function bindFactory(address _factory) external {
        require(factory == address(0) && _factory != address(0), "bound");
        factory = _factory;
    }

    modifier onlyFactory() {
        require(msg.sender == factory, "not factory");
        _;
    }
}

contract BangTokenDeployer is FactoryBound {
    function deploy(string calldata name_, string calldata symbol_, address mintTo)
        external onlyFactory returns (address)
    {
        return address(new BangToken(name_, symbol_, mintTo));
    }
}

contract BangVaultDeployer is FactoryBound {
    function deploy(
        address token, address hook, address universalRouter, address weth, address usdg,
        address[] calldata rewardTokens, uint16[] calldata weightsBps, bytes[] calldata routes
    ) external onlyFactory returns (address) {
        return address(new BangDividendVault(
            token, hook, universalRouter, weth, usdg, rewardTokens, weightsBps, routes
        ));
    }
}
Chain explorer5096msChain node175ms