HoodxLaunchpad

0x3bb5e5e9fa40c2201f76c7ba8fd987aca1196a48

Verification
Verified
v0.8.26+commit.8a97fa7a
Type
Contract
11,888 bytes
ABI entries
23
12 read · 3 write
License
none

Contract information

Address
0x3bb5e5e9fa40c2201f76c7ba8fd987aca1196a48
Chain
Robinhood Chain (4663)
Compiler
v0.8.26+commit.8a97fa7a
Optimization
Enabled
Creation tx
0xbf24c1d074…f63c71edca

Token

Not a token

This contract does not expose ERC-20 metadata.

Read contract (12)

COIN_SUPPLY()uint256
LP_FEE()uint24
TICK_SPACING()int24
allCoins()address[]
coinCount()uint256
coins(uint256)address
feeHook()address
isLaunchpadCoin(address)bool
poolKeyFor(address)tuple
poolManager()address
startFdvEth()uint256
treasury()address

Events (2)

CoinCreatedLpFeesCollected

ABI

[
  {
    "inputs": [
      {
        "internalType": "contract IPoolManager",
        "name": "_poolManager",
        "type": "address"
      },
      {
        "internalType": "contract IHooks",
        "name": "_feeHook",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "_treasury",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "_startFdvEth",
        "type": "uint256"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "inputs": [],
    "name": "NotPoolManager",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ReentrancyGuardReentrantCall",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "SafeERC20FailedOperation",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "UnknownCoin",
    "type": "error"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "coin",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "creator",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "string",
        "name": "name",
        "type": "string"
      },
      {
        "indexed": false,
        "internalType": "string",
        "name": "symbol",
        "type": "string"
      },
      {
        "indexed": false,
        "internalType": "string",
        "name": "metadataURI",
        "type": "string"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "devBuyEth",
        "type": "uint256"
      }
    ],
    "name": "CoinCreated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "coin",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "ethAmount",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "coinAmount",
        "type": "uint256"
      }
    ],
    "name": "LpFeesCollected",
    "type": "event"
  },
  {
    "inputs": [],
    "name": "COIN_SUPPLY",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "LP_FEE",
    "outputs": [
      {
        "internalType": "uint24",
        "name": "",
        "type": "uint24"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "TICK_SPACING",
    "outputs": [
      {
        "internalType": "int24",
        "name": "",
        "type": "int24"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "allCoins",
    "outputs": [
      {
        "internalType": "address[]",
        "name": "",
        "type": "address[]"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "coinCount",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "name": "coins",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "coin",
        "type": "address"
      }
    ],
    "name": "collectLpFees",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "string",
        "name": "name",
        "type": "string"
      },
      {
        "internalType": "string",
        "name": "symbol",
        "type": "string"
      },
      {
        "internalType": "string",
        "name": "metadataURI",
        "type": "string"
      }
    ],
    "name": "createCoin",
    "outputs": [
      {
        "internalType": "address",
        "name": "coin",
        "type": "address"
      }
    ],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "feeHook",
    "outputs": [
      {
        "internalType": "contract IHooks",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "isLaunchpadCoin",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "coin",
        "type": "address"
      }
    ],
    "name": "poolKeyFor",
    "outputs": [
      {
        "components": [
          {
            "internalType": "Currency",
            "name": "currency0",
            "type": "address"
          },
          {
            "internalType": "Currency",
            "name": "currency1",
            "type": "address"
          },
          {
            "internalType": "uint24",
            "name": "fee",
            "type": "uint24"
          },
          {
            "internalType": "int24",
            "name": "tickSpacing",
            "type": "int24"
          },
          {
            "internalType": "contract IHooks",
            "name": "hooks",
            "type": "address"
          }
        ],
        "internalType": "struct PoolKey",
        "name": "",
        "type": "tuple"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "poolManager",
    "outputs": [
      {
        "internalType": "contract IPoolManager",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "startFdvEth",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "treasury",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes",
        "name": "data",
        "type": "bytes"
      }
    ],
    "name": "unlockCallback",
    "outputs": [
      {
        "internalType": "bytes",
        "name": "",
        "type": "bytes"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "stateMutability": "payable",
    "type": "receive"
  }
]

Source code

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

import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";

import {IPoolManager} from "v4-core/src/interfaces/IPoolManager.sol";
import {IHooks} from "v4-core/src/interfaces/IHooks.sol";
import {IUnlockCallback} from "v4-core/src/interfaces/callback/IUnlockCallback.sol";
import {PoolKey} from "v4-core/src/types/PoolKey.sol";
import {Currency} from "v4-core/src/types/Currency.sol";
import {BalanceDelta} from "v4-core/src/types/BalanceDelta.sol";
import {ModifyLiquidityParams, SwapParams} from "v4-core/src/types/PoolOperation.sol";
import {TickMath} from "v4-core/src/libraries/TickMath.sol";
import {FullMath} from "v4-core/src/libraries/FullMath.sol";

import {LaunchCoin} from "./LaunchCoin.sol";

/// @title HoodxLaunchpad — bonding curve av laast Uniswap v4-likviditet
/// @notice Én transaksjon: deploy memecoin -> opprett pool med LaunchFeeHook
///         (4 %% -> creator rewards + HDX-treasury) -> HELE supplyen legges
///         som ENSIDIG likviditet fra en gulvpris og oppover (AUDIT-fix M-4).
///
///         KURVEN: startprisen tilsvarer START_FDV (f.eks. 2 ETH for hele
///         supplyen). Kjoepernes ETH akkumuleres i den laaste posisjonen
///         etter hvert som prisen klatrer — én ensidig full-range-posisjon
///         oppfoerer seg som en bonding curve med virtuell dybde = START_FDV.
///         Ingen kan snipe billig supply: foerste X ETH kjoeper ~X/(FDV+X)
///         av supplyen, aldri mer.
///
///         LAAST BY CONSTRUCTION: posisjonen eies av denne kontrakten, som
///         ikke har noen funksjon for aa fjerne likviditet. LP-gebyrer kan
///         hoestes av hvem som helst — alltid til HDX-treasury.
///
///         DEV BUY: creator kan sende ETH med createCoin og faa foerste
///         kjoep i samme transaksjon (betaler samme 4 %% fee som alle andre).
///
/// @dev Prisretning i v4: pris = token1/token0 (coin per ETH). At coinen
///      STIGER i verdi = faerre coin per ETH = tick NED. Buys er zeroForOne.
///      Kurve-omraadet er derfor [minUsableTick, startTick] og poolen
///      initialiseres PAA startTick (posisjonen er da 100 %% coin).
contract HoodxLaunchpad is IUnlockCallback, ReentrancyGuard {
    using SafeERC20 for IERC20;

    IPoolManager public immutable poolManager;
    IHooks public immutable feeHook;
    address public immutable treasury;

    uint256 public constant COIN_SUPPLY = 1_000_000_000e18; // 1 mrd per coin
    uint24 public constant LP_FEE = 3000;                    // 0,3 %% LP-fee (til treasury)
    int24 public constant TICK_SPACING = 60;

    /// @notice Virtuell startdybde: FDV ved gulvprisen (eier-valgt, f.eks. 2 ETH).
    uint256 public immutable startFdvEth;

    // ---- Register ----
    address[] public coins;
    mapping(address => PoolKey) private poolKeyOf;
    mapping(address => bool) public isLaunchpadCoin;

    event CoinCreated(
        address indexed coin,
        address indexed creator,
        string name,
        string symbol,
        string metadataURI,
        uint256 devBuyEth
    );
    event LpFeesCollected(address indexed coin, uint256 ethAmount, uint256 coinAmount);

    error UnknownCoin();
    error NotPoolManager();

    uint8 private constant ACTION_ADD_LIQUIDITY = 1;
    uint8 private constant ACTION_COLLECT_FEES = 2;

    constructor(IPoolManager _poolManager, IHooks _feeHook, address _treasury, uint256 _startFdvEth) {
        require(_startFdvEth >= 0.1 ether, "start FDV too low"); // snipe-vern
        poolManager = _poolManager;
        feeHook = _feeHook;
        treasury = _treasury;
        startFdvEth = _startFdvEth;
    }

    receive() external payable {} // ETH fra poolManager.take ved fee-hoesting

    // ------------------------------------------------------------------
    //  Launch
    // ------------------------------------------------------------------

    /// @notice Lag en memecoin. msg.value (valgfritt) blir creators foerste
    ///         kjoep gjennom kurven — samme vilkaar som alle andre.
    function createCoin(
        string calldata name,
        string calldata symbol,
        string calldata metadataURI
    ) external payable nonReentrant returns (address coin) {
        coin = address(new LaunchCoin(name, symbol, metadataURI, msg.sender, COIN_SUPPLY));

        PoolKey memory key = PoolKey({
            currency0: Currency.wrap(address(0)),
            currency1: Currency.wrap(coin),
            fee: LP_FEE,
            tickSpacing: TICK_SPACING,
            hooks: feeHook
        });
        poolKeyOf[coin] = key;
        isLaunchpadCoin[coin] = true;
        coins.push(coin);

        // Gulvprisen: som om hele supplyen var backet av startFdvEth.
        (, int24 startTick) = _bondRange();
        poolManager.initialize(key, TickMath.getSqrtPriceAtTick(startTick));

        poolManager.unlock(abi.encode(ACTION_ADD_LIQUIDITY, coin, msg.value, msg.sender));

        emit CoinCreated(coin, msg.sender, name, symbol, metadataURI, msg.value);
    }

    /// @notice Hoest LP-gebyrer fra en launchpad-pool til HDX-treasury.
    function collectLpFees(address coin) external nonReentrant {
        if (!isLaunchpadCoin[coin]) revert UnknownCoin();
        poolManager.unlock(abi.encode(ACTION_COLLECT_FEES, coin, uint256(0), address(0)));
    }

    // ------------------------------------------------------------------
    //  PoolManager-callback
    // ------------------------------------------------------------------

    function unlockCallback(bytes calldata data) external returns (bytes memory) {
        if (msg.sender != address(poolManager)) revert NotPoolManager();
        (uint8 action, address coin, uint256 devBuyEth, address creator) =
            abi.decode(data, (uint8, address, uint256, address));
        PoolKey memory key = poolKeyOf[coin];
        (int24 tickLower, int24 tickUpper) = _bondRange();

        if (action == ACTION_ADD_LIQUIDITY) {
            // Ensidig coin-likviditet [minTick, startTick]; pris staar PAA
            // startTick, saa posisjonen krever kun token1 (coin).
            uint160 sqrtLower = TickMath.getSqrtPriceAtTick(tickLower);
            uint160 sqrtUpper = TickMath.getSqrtPriceAtTick(tickUpper);
            uint256 liquidity = FullMath.mulDiv(COIN_SUPPLY, 1 << 96, sqrtUpper - sqrtLower);
            // Kjernen runder innskudd OPP — margin saa vi aldri skylder mer
            // coin enn vi holder.
            if (liquidity > 1000) liquidity -= 1000;

            (BalanceDelta mod,) = poolManager.modifyLiquidity(
                key,
                ModifyLiquidityParams({
                    tickLower: tickLower,
                    tickUpper: tickUpper,
                    liquidityDelta: int256(liquidity),
                    salt: bytes32(0)
                }),
                ""
            );
            // Betal coin-siden (ETH-siden er 0 ved start paa oevre tick).
            if (mod.amount1() < 0) {
                poolManager.sync(key.currency1);
                IERC20(coin).safeTransfer(
                    address(poolManager), uint256(uint128(-mod.amount1()))
                );
                poolManager.settle();
            }

            // Valgfritt dev buy: creators foerste kjoep gjennom kurven.
            if (devBuyEth > 0) {
                BalanceDelta s = poolManager.swap(
                    key,
                    SwapParams({
                        zeroForOne: true,
                        amountSpecified: -int256(devBuyEth),
                        sqrtPriceLimitX96: TickMath.MIN_SQRT_PRICE + 1
                    }),
                    ""
                );
                if (s.amount0() < 0) {
                    poolManager.settle{value: uint256(uint128(-s.amount0()))}();
                }
                if (s.amount1() > 0) {
                    poolManager.take(key.currency1, creator, uint256(uint128(s.amount1())));
                }
            }
        } else if (action == ACTION_COLLECT_FEES) {
            (BalanceDelta delta,) = poolManager.modifyLiquidity(
                key,
                ModifyLiquidityParams({
                    tickLower: tickLower,
                    tickUpper: tickUpper,
                    liquidityDelta: 0,
                    salt: bytes32(0)
                }),
                ""
            );
            uint256 ethOut;
            uint256 coinOut;
            if (delta.amount0() > 0) {
                ethOut = uint256(uint128(delta.amount0()));
                poolManager.take(key.currency0, treasury, ethOut);
            }
            if (delta.amount1() > 0) {
                coinOut = uint256(uint128(delta.amount1()));
                poolManager.take(key.currency1, treasury, coinOut);
            }
            emit LpFeesCollected(coin, ethOut, coinOut);
        }
        return "";
    }

    // ------------------------------------------------------------------
    //  Views
    // ------------------------------------------------------------------

    function coinCount() external view returns (uint256) {
        return coins.length;
    }

    function allCoins() external view returns (address[] memory) {
        return coins;
    }

    function poolKeyFor(address coin) external view returns (PoolKey memory) {
        if (!isLaunchpadCoin[coin]) revert UnknownCoin();
        return poolKeyOf[coin];
    }

    /// @notice Kurve-omraadet (likt for alle coins: samme supply og start-FDV).
    function _bondRange() internal view returns (int24 tickLower, int24 tickUpper) {
        tickLower = TickMath.minUsableTick(TICK_SPACING);
        // P_start = COIN_SUPPLY / startFdvEth (coin per ETH ved gulvet).
        uint256 ratioX192 = FullMath.mulDiv(COIN_SUPPLY, 1 << 192, startFdvEth);
        // forge-lint: disable-next-line(unsafe-typecast)
        uint160 sqrtStart = uint160(_sqrt(ratioX192)); // trygt: < MAX_SQRT_PRICE for alle rimelige FDV
        int24 raw = TickMath.getTickAtSqrtPrice(sqrtStart);
        tickUpper = _floorToSpacing(raw, TICK_SPACING);
    }

    function _floorToSpacing(int24 tick, int24 spacing) internal pure returns (int24) {
        int24 q = tick / spacing;
        if (tick < 0 && tick % spacing != 0) q--;
        return q * spacing;
    }

    function _sqrt(uint256 x) internal pure returns (uint256 y) {
        if (x == 0) return 0;
        uint256 z = (x + 1) / 2;
        y = x;
        while (z < y) {
            y = z;
            z = (x / z + z) / 2;
        }
    }
}
Chain explorer3236msChain node96ms