LaunchSERC20

Description:

Multi-signature wallet contract requiring multiple confirmations for transaction execution.

Blockchain: Ethereum

Source Code: View Code On The Blockchain

Solidity Source Code:

{{
  "language": "Solidity",
  "sources": {
    "src/LaunchSERC20.sol": {
      "content": "// SPDX-License-Identifier: UNLICENSED

// The first ERC20 strategy launchpad.
// https://sterc20.xyz/
// https://x.com/sterc20/

pragma solidity ^0.8.13;
import {SERC20} from "./SERC20.sol";
import {Ownable} from "@solady/auth/Ownable.sol";
import {ReentrancyGuard} from "@solady/utils/ReentrancyGuard.sol";
import {SafeTransferLib} from "@solady/utils/SafeTransferLib.sol";
import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol";
import {IPositionManager} from "@uniswap/v4-periphery/src/interfaces/IPositionManager.sol";
import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
import {IUniswapV2Router02} from "@uniswap/v2-periphery/contracts/interfaces/IUniswapV2Router02.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {IUniswapV3Pool} from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol";
import {IUniversalRouter} from "../lib/universal-router/contracts/interfaces/IUniversalRouter.sol";
import {Commands} from "../lib/universal-router/contracts/libraries/Commands.sol";
import {IV4Router} from "@uniswap/v4-periphery/src/interfaces/IV4Router.sol";
import {Currency} from "@uniswap/v4-core/src/types/Currency.sol";
import {Actions} from "@uniswap/v4-periphery/src/libraries/Actions.sol";
import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol";

interface ISwapRouter02 {
    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);
}

contract LaunchSERC20 is Ownable, ReentrancyGuard {
    using StateLibrary for IPoolManager;
    using PoolIdLibrary for PoolKey;
    /* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ */
    /*                      CONSTANTS                      */
    /* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ */

    uint256 private constant MIN_ETH_THRESHOLD = 1 ether;

    // Core protocol addresses
    IPositionManager private constant POSM =
        IPositionManager(0xbD216513d74C8cf14cf4747E6AaA6420FF64ee9e);

    // Uniswap routers
    IUniswapV2Router02 private constant UNISWAP_V2_ROUTER =
        IUniswapV2Router02(0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D);
    IUniversalRouter private constant UNIVERSAL_ROUTER =
        IUniversalRouter(0x66a9893cC07D91D95644AEDD05D03f95e1dBA8Af);
    address private constant SWAP_ROUTER_02 =
        0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45;

    // DEAD ADDRESS
    address private constant DEAD_ADDRESS =
        0x000000000000000000000000000000000000dEaD;

    /* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ */
    /*                    STATE VARIABLES                   */
    /* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ */

    // Array to store all deployed SERC20 contracts
    address[] public deployedStrategies;

    // Mapping from strategy token to deployed SERC20 contract
    mapping(address => address) public strategyToSERC20;

    // Mapping to check if a strategy token already has a deployment
    mapping(address => bool) public isStrategyDeployed;

    // Mapping from SERC20 contract to original deployer
    mapping(address => address) public serc20ToDeployer;

    // Structure to store token information
    struct TokenInfo {
        address strategyToken;
        string ammVersion;
        address pairAddress;
        uint24 poolFee; // For V3 only, ignored for V2
        string name;
        string symbol;
        address deployer;
    }

    // Mapping from SERC20 address to TokenInfo
    mapping(address => TokenInfo) public tokenInfoBySERC20;

    // Purchase tracking per SERC20 contract
    mapping(address => uint256) public nextPurchaseId;
    mapping(address => mapping(uint256 => Purchase)) public purchases;

    struct Purchase {
        string method; // "V2", "V3"
        address poolAddress; // Address of the pool where it was purchased
        uint256 tokenAmount; // Amount of strategy tokens received
        uint256 ethAmount; // Amount of ETH spent
        uint256 purchasePrice; // Price per token (ethAmount / tokenAmount)
        uint256 timestamp; // When the purchase was made
        bool sold; // Whether this purchase has been sold
    }

    /* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ */
    /*                    CUSTOM EVENTS                    */
    /* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ */

    event StrategyLaunched(
        address indexed strategyToken,
        address indexed serc20Contract,
        string name,
        string symbol,
        address indexed deployer
    );

    event StrategyTokenPurchased(
        address indexed strategyToken,
        address indexed recipient,
        uint256 ethAmount,
        string version
    );

    event SERC20Burned(
        uint256 ethUsed,
        uint256 serc20Received,
        uint256 serc20Burned
    );

    event PurchaseTracked(
        address indexed serc20Address,
        uint256 indexed purchaseId,
        string method,
        address poolAddress,
        uint256 tokenAmount,
        uint256 ethAmount,
        uint256 purchasePrice
    );

    /* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ */
    /*                    CUSTOM ERRORS                    */
    /* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ */

    error StrategyAlreadyDeployed();
    error InvalidStrategyToken();
    error EmptyName();
    error EmptySymbol();
    error PoolNotInitialized();
    error InsufficientETHFees();
    error TokenNotDeployed();
    error InvalidAMMVersion();

    /* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ */
    /*                     CONSTRUCTOR                     */
    /* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ */

    constructor(address owner_) {
        _initializeOwner(owner_);
    }

    /* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ */
    /*                     FUNCTIONS                       */
    /* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ */

    /// @notice Launch a new SERC20 strategy contract
    /// @param strategyToken The address of the strategy token to be used for buybacks
    /// @param name The name of the new SERC20 token
    /// @param symbol The symbol of the new SERC20 token
    /// @param ammVersion The AMM version where the strategyToken is traded ("V2" or "V3")
    /// @param pairAddress The address of the trading pair
    /// @param poolFee The pool fee for V3 (500, 3000, or 10000), ignored for V2
    /// @return serc20Address The address of the newly deployed SERC20 contract
    function LaunchStrategy(
        address strategyToken,
        string memory name,
        string memory symbol,
        string memory ammVersion,
        address pairAddress,
        uint24 poolFee
    ) external nonReentrant returns (address serc20Address) {
        if (strategyToken == address(0)) revert InvalidStrategyToken();
        if (bytes(name).length == 0) revert EmptyName();
        if (bytes(symbol).length == 0) revert EmptySymbol();
        if (isStrategyDeployed[strategyToken]) revert StrategyAlreadyDeployed();

        // 1. Deploy the SERC20
        SERC20 newSERC20 = new SERC20(
            name,
            symbol,
            strategyToken,
            msg.sender,
            address(this)
        );

        serc20Address = address(newSERC20);

        newSERC20.loadLiquidity(address(this));

        // 3. Update mappings
        deployedStrategies.push(serc20Address);
        strategyToSERC20[strategyToken] = serc20Address;
        isStrategyDeployed[strategyToken] = true;
        serc20ToDeployer[serc20Address] = msg.sender;

        // Store the info in the structure
        tokenInfoBySERC20[serc20Address] = TokenInfo({
            strategyToken: strategyToken,
            ammVersion: ammVersion,
            pairAddress: pairAddress,
            poolFee: poolFee,
            name: name,
            symbol: symbol,
            deployer: msg.sender
        });

        // 4. Emit event
        emit StrategyLaunched(
            strategyToken,
            serc20Address,
            name,
            symbol,
            msg.sender
        );

        return serc20Address;
    }

    /// @notice Get the number of deployed strategies
    /// @return count The total number of deployed SERC20 contracts
    function getDeployedStrategiesCount()
        external
        view
        returns (uint256 count)
    {
        return deployedStrategies.length;
    }

    /// @notice Get all deployed strategy addresses
    /// @return strategies Array of all deployed SERC20 contract addresses
    function getAllDeployedStrategies()
        external
        view
        returns (address[] memory strategies)
    {
        return deployedStrategies;
    }

    /// @notice Get deployed strategies with pagination
    /// @param offset The starting index
    /// @param limit The maximum number of strategies to return
    /// @return strategies Array of SERC20 contract addresses
    function getDeployedStrategies(
        uint256 offset,
        uint256 limit
    ) external view returns (address[] memory strategies) {
        uint256 total = deployedStrategies.length;

        if (offset >= total) {
            return new address[](0);
        }

        uint256 end = offset + limit;
        if (end > total) {
            end = total;
        }

        strategies = new address[](end - offset);
        for (uint256 i = offset; i < end; i++) {
            strategies[i - offset] = deployedStrategies[i];
        }

        return strategies;
    }

    /// @notice Check if a strategy token has been deployed
    /// @param strategyToken The strategy token address to check
    /// @return deployed True if the strategy has been deployed, false otherwise
    function isStrategyAlreadyDeployed(
        address strategyToken
    ) external view returns (bool deployed) {
        return isStrategyDeployed[strategyToken];
    }

    /// @notice Get the SERC20 contract address for a given strategy token
    /// @param strategyToken The strategy token address
    /// @return serc20Address The corresponding SERC20 contract address (address(0) if not deployed)
    function getSERC20ForStrategy(
        address strategyToken
    ) external view returns (address serc20Address) {
        return strategyToSERC20[strategyToken];
    }

    /// @notice Get the original deployer of a SERC20 contract
    /// @param serc20Address The SERC20 contract address
    /// @return deployer The address that originally launched this strategy
    function getStrategyDeployer(
        address payable serc20Address
    ) external view returns (address deployer) {
        return serc20ToDeployer[serc20Address];
    }

    /// @notice Get token info by SERC20 address
    /// @param serc20Address The SERC20 contract address
    /// @return info The TokenInfo structure containing all details
    function getTokenInfoByAddress(
        address serc20Address
    ) external view returns (TokenInfo memory info) {
        return tokenInfoBySERC20[serc20Address];
    }

    /// @notice Emergency claim: collect accumulated fees for a given pool key and SERC20 address and send to owner
    /// @param poolKey The pool key to collect fees from
    /// @param serc20Address The SERC20 contract address
    /// @return ethFees Amount of ETH fees collected
    /// @return tokenFees Amount of token fees collected
    function emergencyClaim(
        PoolKey memory poolKey,
        address payable serc20Address
    )
        external
        onlyOwner
        nonReentrant
        returns (uint256 ethFees, uint256 tokenFees)
    {
        // Verify that the token exists on the launchpad
        TokenInfo memory tokenInfo = tokenInfoBySERC20[serc20Address];
        if (tokenInfo.strategyToken == address(0)) revert TokenNotDeployed();

        // Use internal function to collect fees to this contract
        (ethFees, tokenFees) = _collectAccumulatedFees(poolKey, serc20Address);

        // Transfer collected ETH to contract owner
        if (ethFees > 0) {
            SafeTransferLib.safeTransferETH(owner(), ethFees);
        }

        // Transfer collected SERC20 tokens to contract owner
        if (tokenFees > 0) {
            require(
                IERC20(serc20Address).transfer(owner(), tokenFees),
                "Token transfer to owner failed"
            );
        }

        return (ethFees, tokenFees);
    }

    /// @notice Allow owner to recover any ERC20 tokens stored in the contract
    /// @param token The address of the ERC20 token to recover
    /// @param amount The amount of tokens to recover (0 to recover all)
    function recoverERC20(
        address token,
        uint256 amount
    ) external onlyOwner nonReentrant {
        require(token != address(0), "Invalid token address");
        
        IERC20 tokenContract = IERC20(token);
        uint256 balance = tokenContract.balanceOf(address(this));
        require(balance > 0, "No tokens to recover");
        
        // If amount is 0, recover all tokens
        uint256 amountToRecover = amount == 0 ? balance : amount;
        require(amountToRecover <= balance, "Insufficient token balance");
        
        // Transfer tokens to owner
        require(
            tokenContract.transfer(owner(), amountToRecover),
            "Token transfer failed"
        );
    }

    /* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ */
    /*                      POOL FUNCTIONS                 */
    /* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ */

    /// @notice Get accumulated fees in the pool using StateLibrary
    /// @param poolKey The pool key to check fees for
    /// @return fees0 Global fee growth for token0 (ETH)
    /// @return fees1 Global fee growth for token1 (SERC20 token)
    function getAccumulatedFees(
        PoolKey memory poolKey
    ) external view returns (uint256 fees0, uint256 fees1) {
        // Get the SERC20 contract address from the poolKey
        address serc20Address = Currency.unwrap(poolKey.currency1);
        SERC20 serc20 = SERC20(payable(serc20Address));

        // Get the position token ID and tick range
        uint256 tokenId = serc20.getPositionTokenId();
        (int24 tickLower, int24 tickUpper) = serc20.getTickRange();

        // Get pool manager
        IPoolManager poolManager = POSM.poolManager();
        PoolId poolId = poolKey.toId();

        // Get position info from pool manager
        // The position is owned by POSM (position manager) with tokenId as salt
        (
            uint128 liquidity,
            uint256 feeGrowthInside0LastX128,
            uint256 feeGrowthInside1LastX128
        ) = poolManager.getPositionInfo(
                poolId,
                address(POSM),
                tickLower,
                tickUpper,
                bytes32(tokenId)
            );

        // Get current fee growth inside the position range using StateLibrary
        (
            uint256 feeGrowthInside0X128,
            uint256 feeGrowthInside1X128
        ) = poolManager.getFeeGrowthInside(poolId, tickLower, tickUpper);

        // Calculate fees owed using the same formula as Uniswap
        fees0 =
            ((feeGrowthInside0X128 - feeGrowthInside0LastX128) * liquidity) /
            (1 << 128);
        fees1 =
            ((feeGrowthInside1X128 - feeGrowthInside1LastX128) * liquidity) /
            (1 << 128);

        return (fees0, fees1);
    }

    /// @notice Get pool information for a SERC20 contract
    /// @param serc20Address The SERC20 contract address
    /// @return initialized Whether the pool has been initialized
    /// @return poolKeyData The pool key data
    /// @return poolIdData The pool ID
    function getPoolInfo(
        address payable serc20Address
    )
        external
        view
        returns (
            bool initialized,
            PoolKey memory poolKeyData,
            PoolId poolIdData
        )
    {
        SERC20 serc20 = SERC20(payable(serc20Address));
        return serc20.getPoolInfo();
    }

    /* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ */
    /*                   STRATEGY FUNCTIONS                 */
    /* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ */

    /// @notice Buy strategy tokens using accumulated fees - automatically detects AMM version
    /// @param serc20Address The SERC20 contract address
    function buyStrategy(address payable serc20Address) external nonReentrant {
        // Verify that the token exists on the launchpad
        TokenInfo memory tokenInfo = tokenInfoBySERC20[serc20Address];
        if (tokenInfo.strategyToken == address(0)) revert TokenNotDeployed();

        // Retrieve stored info from deployment
        string memory ammVersion = tokenInfo.ammVersion;
        address pairStrategyAddress = tokenInfo.pairAddress;
        uint24 poolFee = tokenInfo.poolFee;

        // Route to appropriate function based on AMM
        if (
            keccak256(abi.encodePacked(ammVersion)) ==
            keccak256(abi.encodePacked("V2"))
        ) {
            buyStrategyTokenV2(serc20Address, pairStrategyAddress);
        } else if (
            keccak256(abi.encodePacked(ammVersion)) ==
            keccak256(abi.encodePacked("V3"))
        ) {
            buyStrategyTokenV3(serc20Address, pairStrategyAddress, poolFee);
        } else {
            revert InvalidAMMVersion();
        }
    }

    /// @notice Buy strategy tokens using accumulated fees via Uniswap V2
    /// @param serc20Address The SERC20 contract address
    /// @param pairStrategyAddress Address where to send the purchased strategy tokens
    function buyStrategyTokenV2(
        address payable serc20Address,
        address pairStrategyAddress
    ) internal {
        SERC20 serc20 = SERC20(payable(serc20Address));

        // Get pool info
        (bool poolInitialized, PoolKey memory poolKey, ) = serc20.getPoolInfo();
        if (!poolInitialized) revert PoolNotInitialized();

        // Check fees threshold
        (uint256 ethFees, ) = this.getAccumulatedFees(poolKey);
        if (ethFees < MIN_ETH_THRESHOLD) revert InsufficientETHFees();

        // Get accumulated fees and collect them
        (uint256 ethFeesCollected, uint256 tokenFees) = _collectAccumulatedFees(
            poolKey,
            serc20Address
        );

        // Calculate distributions
        (
            uint256 ethForSwap,
            uint256 ethToLaunchpad,
            uint256 ethToOriginalDeployer,
            uint256 tokensToLaunchpad,
            uint256 tokensToBurn
        ) = _calculateDistributions(ethFeesCollected, tokenFees);

        // Get strategy token address from SERC20
        address strategyToken = serc20.strategyToken();

        // Swap ETH for strategy tokens on Uniswap V2 with 10% slippage
        if (ethForSwap > 0) {
            address[] memory path = new address[](2);
            path[0] = UNISWAP_V2_ROUTER.WETH();
            path[1] = strategyToken;

            // Get expected output amount
            uint[] memory amounts = UNISWAP_V2_ROUTER.getAmountsOut(
                ethForSwap,
                path
            );
            uint256 expectedTokens = amounts[1];

            // Apply 10% slippage (accept 90% of expected amount)
            uint256 minTokensOut = (expectedTokens * 90) / 100;

            uint[] memory swapAmounts = UNISWAP_V2_ROUTER.swapExactETHForTokens{
                value: ethForSwap
            }(minTokensOut, path, serc20Address, block.timestamp + 300);

            uint256 tokensReceived = swapAmounts[1];

            // Track the purchase for future selling
            _trackPurchase(
                serc20Address,
                "V2",
                pairStrategyAddress,
                tokensReceived,
                ethForSwap
            );

            emit StrategyTokenPurchased(
                strategyToken,
                serc20Address,
                ethForSwap,
                "V2"
            );
        }

        // Distribute remaining funds
        _distributeFunds(
            serc20Address,
            ethToLaunchpad,
            ethToOriginalDeployer,
            tokensToLaunchpad,
            tokensToBurn
        );
    }

    /// @notice Buy strategy tokens using accumulated fees via Uniswap V3
    /// @param serc20Address The SERC20 contract address
    /// @param pairStrategyAddress Address of the Uniswap V3 pool (not used in this version)
    /// @param poolFee The pool fee for the V3 swap (500, 3000, or 10000)
    function buyStrategyTokenV3(
        address payable serc20Address,
        address pairStrategyAddress,
        uint24 poolFee
    ) internal {
        SERC20 serc20 = SERC20(payable(serc20Address));

        // Get pool info
        (bool poolInitialized, PoolKey memory poolKey, ) = serc20.getPoolInfo();
        if (!poolInitialized) revert PoolNotInitialized();

        // Check fees threshold
        (uint256 ethFees, ) = this.getAccumulatedFees(poolKey);
        if (ethFees < MIN_ETH_THRESHOLD) revert InsufficientETHFees();

        // Get accumulated fees and collect them
        (uint256 ethFeesCollected, uint256 tokenFees) = _collectAccumulatedFees(
            poolKey,
            serc20Address
        );

        // Calculate distributions
        (
            uint256 ethForSwap,
            uint256 ethToLaunchpad,
            uint256 ethToOriginalDeployer,
            uint256 tokensToLaunchpad,
            uint256 tokensToBurn
        ) = _calculateDistributions(ethFeesCollected, tokenFees);

        // Get strategy token address from SERC20
        address strategyToken = serc20.strategyToken();

        // Swap ETH for strategy tokens on Uniswap V3
        if (ethForSwap > 0) {
            address WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;

            uint256 tokensReceived = ISwapRouter02(SWAP_ROUTER_02)
                .exactInputSingle{value: ethForSwap}(
                ISwapRouter02.ExactInputSingleParams({
                    tokenIn: WETH,
                    tokenOut: strategyToken,
                    fee: poolFee,
                    recipient: serc20Address,
                    amountIn: ethForSwap,
                    amountOutMinimum: 0,
                    sqrtPriceLimitX96: 0
                })
            );

            // Track the purchase for future selling
            _trackPurchase(
                serc20Address,
                "V3",
                pairStrategyAddress,
                tokensReceived,
                ethForSwap
            );

            emit StrategyTokenPurchased(
                strategyToken,
                serc20Address,
                ethForSwap,
                "V3"
            );
        }

        // Distribute remaining funds
        _distributeFunds(
            serc20Address,
            ethToLaunchpad,
            ethToOriginalDeployer,
            tokensToLaunchpad,
            tokensToBurn
        );
    }

    /// @notice Sell strategy tokens if price is 2x higher than purchase price
    /// @param serc20Address The SERC20 contract address
    function sellStrategyToken(
        address payable serc20Address
    ) external nonReentrant {
        SERC20 serc20 = SERC20(payable(serc20Address));

        // Get pool info
        (bool poolInitialized, , ) = serc20.getPoolInfo();
        if (!poolInitialized) revert PoolNotInitialized();

        uint256 totalEthReceived = 0;
        address strategyToken = serc20.strategyToken();

        uint256 nextPurchaseIdLocal = nextPurchaseId[serc20Address];

        for (uint256 i = 0; i < nextPurchaseIdLocal; i++) {
            Purchase memory purchase = purchases[serc20Address][i];

            string memory method = purchase.method;
            address poolAddress = purchase.poolAddress;
            uint256 tokenAmount = purchase.tokenAmount;
            uint256 ethAmount = purchase.ethAmount;
            uint256 purchasePrice = purchase.purchasePrice;
            bool sold = purchase.sold;

            if (sold || tokenAmount == 0) continue;

            uint256 currentPrice = _getCurrentPrice(
                method,
                strategyToken,
                poolAddress
            );
            if (currentPrice == 0) continue;

            // Check if current price is 2x the purchase price
            if (currentPrice >= purchasePrice * 2) {
                // Sell the tokens
                uint256 ethReceived = _sellStrategyTokens(
                    method,
                    strategyToken,
                    tokenAmount
                );
                totalEthReceived += ethReceived;

                // Mark this purchase as sold
                purchases[serc20Address][i].sold = true;
            }
        }

        // If we received ETH from sales, buy SERC20 tokens and burn them
        if (totalEthReceived > 0) {
            _buyAndBurnSERC20(serc20Address, totalEthReceived);
        }
    }

    /* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ */
    /*                   INTERNAL FUNCTIONS                 */
    /* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ */

    /// @notice Track a strategy token purchase
    function _trackPurchase(
        address serc20Address,
        string memory method,
        address poolAddress,
        uint256 tokenAmount,
        uint256 ethAmount
    ) internal {
        uint256 purchaseId = nextPurchaseId[serc20Address]++;
        uint256 purchasePrice = (ethAmount * 1e18) / tokenAmount; // Price per token in wei

        purchases[serc20Address][purchaseId] = Purchase({
            method: method,
            poolAddress: poolAddress,
            tokenAmount: tokenAmount,
            ethAmount: ethAmount,
            purchasePrice: purchasePrice,
            timestamp: block.timestamp,
            sold: false
        });

        emit PurchaseTracked(
            serc20Address,
            purchaseId,
            method,
            poolAddress,
            tokenAmount,
            ethAmount,
            purchasePrice
        );
    }

    /// @notice Collect accumulated fees from the pool
    /// @param poolKey The pool key
    /// @param serc20Address The SERC20 contract address to collect fees to
    /// @return ethFees Amount of ETH fees collected
    /// @return tokenFees Amount of token fees collected
    function _collectAccumulatedFees(
        PoolKey memory poolKey,
        address payable serc20Address
    ) internal returns (uint256 ethFees, uint256 tokenFees) {
        (ethFees, tokenFees) = this.getAccumulatedFees(poolKey);

        // If no fees, no need to collect
        if (ethFees == 0 && tokenFees == 0) {
            return (0, 0);
        }

        SERC20 serc20 = SERC20(payable(serc20Address));
        uint256 tokenId = serc20.getPositionTokenId();

        // Use DECREASE_LIQUIDITY with 0 liquidity to collect fees only
        bytes memory actions = abi.encodePacked(
            uint8(Actions.DECREASE_LIQUIDITY),
            uint8(Actions.TAKE_PAIR)
        );

        bytes[] memory params = new bytes[](2);

        params[0] = abi.encode(
            tokenId,
            0, // liquidityDelta = 0 (don't remove liquidity, just collect fees)
            0, // amount0Min = 0
            0, // amount1Min = 0
            "" // hookData
        );

        // Parameters for TAKE_PAIR - transfer fees to this contract
        params[1] = abi.encode(
            poolKey.currency0, // ETH
            poolKey.currency1, // SERC20 token
            address(this) // recipient
        );

        // Execute the fee collection through Position Manager
        POSM.modifyLiquidities(
            abi.encode(actions, params),
            block.timestamp + 60
        );

        // The fees are now collected to this contract
        return (ethFees, tokenFees);
    }

    /// @notice Calculate distribution amounts
    /// @return ethForSwap 80% of ETH for swapping
    /// @return ethToLaunchpad 15% of ETH to launchpad
    /// @return ethToOriginalDeployer 5% of ETH to original deployer
    /// @return tokensToLaunchpad 50% of tokens to launchpad
    /// @return tokensToBurn 50% of tokens to burn
    function _calculateDistributions(
        uint256 ethFees,
        uint256 tokenFees
    )
        internal
        pure
        returns (
            uint256 ethForSwap,
            uint256 ethToLaunchpad,
            uint256 ethToOriginalDeployer,
            uint256 tokensToLaunchpad,
            uint256 tokensToBurn
        )
    {
        ethForSwap = (ethFees * 80) / 100;
        ethToLaunchpad = (ethFees * 15) / 100;
        ethToOriginalDeployer = (ethFees * 5) / 100;
        tokensToLaunchpad = (tokenFees * 50) / 100;
        tokensToBurn = (tokenFees * 50) / 100;
    }

    /// @notice Distribute remaining funds after swap
    function _distributeFunds(
        address payable serc20Address,
        uint256 ethToLaunchpad,
        uint256 ethToOriginalDeployer,
        uint256 tokensToLaunchpad,
        uint256 tokensToBurn
    ) internal {
        SERC20 serc20 = SERC20(payable(serc20Address));
        address originalDeployer = serc20ToDeployer[serc20Address];

        // Send 15% ETH to launchpad (this contract)
        if (ethToLaunchpad > 0) {
            SafeTransferLib.safeTransferETH(address(this), ethToLaunchpad);
        }

        // Send 5% ETH to original deployer
        if (ethToOriginalDeployer > 0) {
            SafeTransferLib.safeTransferETH(
                originalDeployer,
                ethToOriginalDeployer
            );
        }

        // Send 50% tokens to dead address (burn)
        if (tokensToBurn > 0) {
            require(
                serc20.transfer(DEAD_ADDRESS, tokensToBurn),
                "Token burn transfer failed"
            );
        }

        // Send 50% tokens to launchpad
        if (tokensToLaunchpad > 0) {
            require(
                serc20.transfer(address(this), tokensToLaunchpad),
                "Token transfer to launchpad failed"
            );
        }
    }

    /// @notice Get current price of strategy token for a specific method
    function _getCurrentPrice(
        string memory method,
        address strategyToken,
        address poolAddress
    ) internal view returns (uint256 currentPrice) {
        if (
            keccak256(abi.encodePacked(method)) ==
            keccak256(abi.encodePacked("V2"))
        ) {
            address[] memory path = new address[](2);
            path[0] = UNISWAP_V2_ROUTER.WETH();
            path[1] = strategyToken;

            uint[] memory amounts = UNISWAP_V2_ROUTER.getAmountsOut(1e18, path);
            return (1e18 * 1e18) / amounts[1]; // Price per token in wei
        } else if (
            keccak256(abi.encodePacked(method)) ==
            keccak256(abi.encodePacked("V3"))
        ) {
            if (poolAddress == address(0)) return 0;

            IUniswapV3Pool pool = IUniswapV3Pool(poolAddress);
            (uint160 sqrtPriceX96, , , , , , ) = pool.slot0();

            // Convert sqrtPriceX96 to price
            // sqrtPriceX96 = sqrt(price) * 2^96
            // price = (sqrtPriceX96 / 2^96)^2
            uint256 sqrtPrice = uint256(sqrtPriceX96);

            // Calculate price with 18 decimal precision
            // price = (sqrtPrice^2 * 1e18) / (2^96)^2
            currentPrice = (sqrtPrice * sqrtPrice * 1e18) >> (96 * 2);

            return currentPrice;
        }

        return 0;
    }

    /// @notice Sell strategy tokens back to the market
    function _sellStrategyTokens(
        string memory method,
        address strategyToken,
        uint256 tokenAmount
    ) internal returns (uint256 ethReceived) {
        // For V2 sales
        if (
            keccak256(abi.encodePacked(method)) ==
            keccak256(abi.encodePacked("V2"))
        ) {
            address[] memory path = new address[](2);
            path[0] = strategyToken;
            path[1] = UNISWAP_V2_ROUTER.WETH();

            // Approve router to spend tokens
            IERC20(strategyToken).approve(
                address(UNISWAP_V2_ROUTER),
                tokenAmount
            );

            // Get expected ETH output
            uint[] memory amounts = UNISWAP_V2_ROUTER.getAmountsOut(
                tokenAmount,
                path
            );
            uint256 expectedETH = amounts[1];
            uint256 minETHOut = (expectedETH * 90) / 100; // 10% slippage

            uint256 ethBefore = address(this).balance;

            UNISWAP_V2_ROUTER.swapExactTokensForETH(
                tokenAmount,
                minETHOut,
                path,
                address(this),
                block.timestamp + 300
            );

            return address(this).balance - ethBefore;
        }

        return 0;
    }

    /// @notice Buy SERC20 tokens with ETH and burn them using Universal Router V4
    function _buyAndBurnSERC20(
        address payable serc20Address,
        uint256 ethAmount
    ) internal {
        if (ethAmount == 0) return;

        SERC20 serc20 = SERC20(payable(serc20Address));

        // Get the SERC20 pool info (ETH/SERC20 V4 pool)
        (bool poolExists, PoolKey memory poolKey, ) = serc20.getPoolInfo();
        if (!poolExists) {
            // Pool not initialized, cannot proceed
            return;
        }

        uint256 minTokensOut = (ethAmount * 9) / 10; // 10% slippage tolerance

        // Track balance before for accurate burn amount calculation
        uint256 contractBalanceBefore = IERC20(serc20Address).balanceOf(
            address(this)
        );

        uint128 safeEthAmount = uint128(ethAmount);
        uint128 safeMinTokensOut = uint128(minTokensOut);
        
        _swapExactInputSingleV4(
            poolKey,
            safeEthAmount,
            safeMinTokensOut
        );

        uint256 contractBalanceAfter = IERC20(serc20Address).balanceOf(
            address(this)
        );
        uint256 tokensReceived = contractBalanceAfter - contractBalanceBefore;

        // Transfer received tokens to burn address
        if (tokensReceived > 0) {
            require(
                IERC20(serc20Address).transfer(DEAD_ADDRESS, tokensReceived),
                "Token burn transfer failed"
            );
        }

        emit SERC20Burned(ethAmount, tokensReceived, tokensReceived);
    }

    function _swapExactInputSingleV4(
        PoolKey memory key,
        uint128 amountIn,
        uint128 minAmountOut
    ) internal returns (uint256 amountOut) {
        // Encode the Universal Router command
        bytes memory commands = abi.encodePacked(uint8(Commands.V4_SWAP));
        bytes[] memory inputs = new bytes[](1);

        // Encode V4Router actions
        bytes memory actions = abi.encodePacked(
            uint8(Actions.SWAP_EXACT_IN_SINGLE),
            uint8(Actions.SETTLE_ALL),
            uint8(Actions.TAKE_ALL)
        );

        // Prepare parameters for each action
        bytes[] memory params = new bytes[](3);
        params[0] = abi.encode(
            IV4Router.ExactInputSingleParams({
                poolKey: key,
                zeroForOne: true, // ETH (currency0) -> SERC20 (currency1)
                amountIn: amountIn,
                amountOutMinimum: minAmountOut,
                hookData: ""
            })
        );
        params[1] = abi.encode(key.currency0, amountIn);
        params[2] = abi.encode(key.currency1, minAmountOut);

        // Combine actions and params into inputs
        inputs[0] = abi.encode(actions, params);

        // Execute the swap with deadline protection
        uint256 deadline = block.timestamp + 300; // 5 minutes
        UNIVERSAL_ROUTER.execute{value: amountIn}(commands, inputs, deadline);

        // Verify and return the output amount
        amountOut = IERC20(Currency.unwrap(key.currency1)).balanceOf(
            address(this)
        );
        require(amountOut >= minAmountOut, "Insufficient output amount");

        return amountOut;
    }

    /// @notice Allows the contract to receive ETH
    receive() external payable {}
}
"
    },
    "src/SERC20.sol": {
      "content": "// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {Ownable} from "@solady/auth/Ownable.sol";
import {ReentrancyGuard} from "@solady/utils/ReentrancyGuard.sol";
import {IAllowanceTransfer} from "@permit2/interfaces/IAllowanceTransfer.sol";
import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
import {Currency} from "@uniswap/v4-core/src/types/Currency.sol";
import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol";
import {IPositionManager} from "@uniswap/v4-periphery/src/interfaces/IPositionManager.sol";
import {Actions} from "@uniswap/v4-periphery/src/libraries/Actions.sol";
import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol";

contract SERC20 is ERC20, Ownable, ReentrancyGuard {
    using PoolIdLibrary for PoolKey;

    /* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ */
    /*                      CONSTANTS                      */
    /* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ */

    uint256 private constant TOTAL_SUPPLY = 1000000000 * 10 ** 18;

    /* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ */
    /*                   HARDCODED ADDRESSES               */
    /* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ */

    // Core protocol addresses
    IAllowanceTransfer private constant PERMIT2 =
        IAllowanceTransfer(0x000000000022D473030F116dDEE9F6B43aC78BA3);
    IPositionManager private constant POSM =
        IPositionManager(0xbD216513d74C8cf14cf4747E6AaA6420FF64ee9e);
    address private constant POOL_MANAGER =
        0x000000000004444c5dc75cB358380D2e3dE08A90;

    /* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ */
    /*                    STATE VARIABLES                   */
    /* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ */

    // Pool state
    bool public loadingLiquidity;
    PoolKey public poolKey;
    PoolId public poolId;
    bool public poolInitialized;
    uint256 public positionTokenId;
    int24 public tickLower;
    int24 public tickUpper;

    // Strategy configuration
    address public immutable strategyToken;
    address public immutable launchpad;
    address public platform;
    address public creator;

    // Launch protection
    uint256 private launchBlock;
    uint256 private maxTxAmount;
    uint256 private constant LAUNCH_PERIOD = 2; // 2 blocks
    uint256 private constant MAX_WALLET_PERCENTAGE = 4; // 4% of total supply during launch

    // Track transfers per tx.origin per block to detect multi-swaps
    mapping(address => uint256) private tokensFromPoolPerOrigin;

    /* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ */
    /*                     CONSTRUCTOR                     */
    /* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ */

    constructor(
        string memory name_,
        string memory symbol_,
        address strategyToken_,
        address creator_,
        address platform_
    ) ERC20(name_, symbol_) {
        _mint(address(this), TOTAL_SUPPLY);
        _initializeOwner(msg.sender);
        strategyToken = strategyToken_;
        launchpad = msg.sender;
        platform = platform_;
        creator = creator_;
        launchBlock = block.number;
        maxTxAmount = (TOTAL_SUPPLY * MAX_WALLET_PERCENTAGE) / 100;
    }

    /// @notice Load initial liquidity into the pool

    function loadLiquidity(address launchpadAddress) external onlyOwner {
        loadingLiquidity = true;

        // Create the pool with ETH (currency0) and TOKEN (currency1)
        Currency currency0 = Currency.wrap(address(0)); // ETH
        Currency currency1 = Currency.wrap(address(this)); // TOKEN

        uint24 lpFee = 100000; // 10% fee
        int24 tickSpacing = 200;

        uint256 token0Amount = 0; // 1 wei
        uint256 token1Amount = 1_000_000_000 * 10 ** 18; // 1B TOKEN

        uint160 startingPrice = 2045645379722529521098596513701367;
        tickLower = int24(-887200);
        tickUpper = int24(203000);

        PoolKey memory key = PoolKey(
            currency0,
            currency1,
            lpFee,
            tickSpacing,
            IHooks(address(0))
        );

        // Store pool information
        poolKey = key;
        poolId = key.toId();
        bytes memory hookData = new bytes(0);

        uint128 liquidity = 39095916497508424169487;

        (
            bytes memory actions,
            bytes[] memory mintParams
        ) = _mintLiquidityParams(
                key,
                tickLower,
                tickUpper,
                liquidity,
                token0Amount,
                token1Amount,
                launchpadAddress,
                hookData
            );

        bytes[] memory params = new bytes[](2);

        params[0] = abi.encodeWithSelector(
            POSM.initializePool.selector,
            key,
            startingPrice,
            hookData
        );

        params[1] = abi.encodeWithSelector(
            POSM.modifyLiquidities.selector,
            abi.encode(actions, mintParams),
            block.timestamp + 60
        );

        uint256 valueToPass = token0Amount;

        // Approve Permit2 to spend our tokens
        _approve(address(this), address(PERMIT2), type(uint256).max);

        PERMIT2.approve(
            address(this),
            address(POSM),
            type(uint160).max,
            type(uint48).max
        );

        // Get the next token ID before minting
        positionTokenId = POSM.nextTokenId();

        POSM.multicall{value: valueToPass}(params);

        loadingLiquidity = false;
        poolInitialized = true;
        poolKey = key;
        poolId = key.toId();
    }

    /* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ */
    /*                      VIEW FUNCTIONS                  */
    /* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ */

    /// @notice Get pool information for this SERC20 token
    /// @return initialized Whether the pool has been initialized
    /// @return poolKeyData The pool key data
    /// @return poolIdData The pool ID
    function getPoolInfo()
        external
        view
        returns (
            bool initialized,
            PoolKey memory poolKeyData,
            PoolId poolIdData
        )
    {
        return (poolInitialized, poolKey, poolId);
    }

    /// @notice Get the position token ID for this SERC20's LP position
    /// @return tokenId The position token ID
    function getPositionTokenId() external view returns (uint256 tokenId) {
        return positionTokenId;
    }

    /// @notice Get the tick range for this SERC20's LP position
    /// @return lower The lower tick bound
    /// @return upper The upper tick bound
    function getTickRange() external view returns (int24 lower, int24 upper) {
        return (tickLower, tickUpper);
    }

    /// @notice Get pool information for V4 integration
    /// @return poolManager The pool manager address
    /// @return tokenAddress This token's address
    /// @return positionManager The position manager address
    function getTokenPair() public view returns (address, address, address) {
        return (POOL_MANAGER, address(this), address(POSM));
    }

    /* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ */
    /*                   INTERNAL FUNCTIONS                 */
    /* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ */

    /// @notice Creates parameters for minting liquidity in Uniswap V4
    function _mintLiquidityParams(
        PoolKey memory key,
        int24 _tickLower,
        int24 _tickUpper,
        uint256 liquidity,
        uint256 amount0Max,
        uint256 amount1Max,
        address recipient,
        bytes memory hookData
    ) internal pure returns (bytes memory, bytes[] memory) {
        bytes memory actions = abi.encodePacked(
            uint8(Actions.MINT_POSITION),
            uint8(Actions.SETTLE_PAIR)
        );

        bytes[] memory params = new bytes[](2);
        params[0] = abi.encode(
            key,
            _tickLower,
            _tickUpper,
            liquidity,
            amount0Max,
            amount1Max,
            recipient,
            hookData
        );
        params[1] = abi.encode(key.currency0, key.currency1);
        return (actions, params);
    }

    /// @notice Override _update to implement launch protection and antibot
    /// @dev Limits transactions to 2% during first 5 blocks and detects multi-swaps
    function _update(
        address from,
        address to,
        uint256 value
    ) internal override {
        // During liquidity loading, allow all transfers
        if (loadingLiquidity) {
            super._update(from, to, value);
            return;
        }

        // Always allow system contract interactions and protocol operations
        if (
            to == address(PERMIT2) ||
            to == address(POSM) ||
            from == address(POSM) ||
            to == POOL_MANAGER ||
            from == POOL_MANAGER ||
            from == address(this) ||
            to == address(this) ||
            from == launchpad ||
            to == launchpad ||
            from == platform ||
            to == platform ||
            from == creator ||
            to == creator ||
            from == address(0) || // Mint operations
            to == address(0)
        ) {
            // Burn operations
            super._update(from, to, value);
            return;
        }

        // Block all other transfers at launch block
        if (block.number == launchBlock) {
            revert("No buys allowed during launch block!");
        }

        // During launch period (blocks 1-5 after launch), apply restrictions to DEX buys only
        if (
            block.number > launchBlock &&
            block.number <= launchBlock + LAUNCH_PERIOD
        ) {
            // Check if this is a buy from V4 pool (tokens coming FROM pool manager TO user)
            bool isV4PoolBuy = (from == POOL_MANAGER &&
                to != platform &&
                to != creator &&
                to != launchpad);

            if (isV4PoolBuy) {
                // Track tokens received from pool per tx.origin to detect multi-swaps
                tokensFromPoolPerOrigin[tx.origin] += value;
                require(
                    tokensFromPoolPerOrigin[tx.origin] <=
                        (maxTxAmount * 110) / 100,
                    "Keeping 4% pool Limits"
                );

                // Apply wallet limits for DEX buys
                require(
                    balanceOf(to) + value <= maxTxAmount,
                    "Max wallet limit exceeded during launch period"
                );
            }
        }

        super._update(from, to, value);
    }

    /// @notice Get the maximum transaction amount during launch period
    /// @return maxTx The maximum transaction amount (2% of total supply)
    function getMaxTxAmount() public view returns (uint256 maxTx) {
        return maxTxAmount;
    }

    /// @notice Allows the contract to receive ETH
    receive() external payable {}
}
"
    },
    "lib/solady/src/auth/Ownable.sol": {
      "content": "// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

/// @notice Simple single owner authorization mixin.
/// @author Solady (https://github.com/vectorized/solady/blob/main/src/auth/Ownable.sol)
///
/// @dev Note:
/// This implementation does NOT auto-initialize the owner to `msg.sender`.
/// You MUST call the `_initializeOwner` in the constructor / initializer.
///
/// While the ownable portion follows
/// [EIP-173](https://eips.ethereum.org/EIPS/eip-173) for compatibility,
/// the nomenclature for the 2-step ownership handover may be unique to this codebase.
abstract contract Ownable {
    /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
    /*                       CUSTOM ERRORS                        */
    /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

    /// @dev The caller is not authorized to call the function.
    error Unauthorized();

    /// @dev The `newOwner` cannot be the zero address.
    error NewOwnerIsZeroAddress();

    /// @dev The `pendingOwner` does not have a valid handover request.
    error NoHandoverRequest();

    /// @dev Cannot double-initialize.
    error AlreadyInitialized();

    /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
    /*                           EVENTS                           */
    /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

    /// @dev The ownership is transferred from `oldOwner` to `newOwner`.
    /// This event is intentionally kept the same as OpenZeppelin's Ownable to be
    /// compatible with indexers and [EIP-173](https://eips.ethereum.org/EIPS/eip-173),
    /// despite it not being as lightweight as a single argument event.
    event OwnershipTransferred(address indexed oldOwner, address indexed newOwner);

    /// @dev An ownership handover to `pendingOwner` has been requested.
    event OwnershipHandoverRequested(address indexed pendingOwner);

    /// @dev The ownership handover to `pendingOwner` has been canceled.
    event OwnershipHandoverCanceled(address indexed pendingOwner);

    /// @dev `keccak256(bytes("OwnershipTransferred(address,address)"))`.
    uint256 private constant _OWNERSHIP_TRANSFERRED_EVENT_SIGNATURE =
        0x8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0;

    /// @dev `keccak256(bytes("OwnershipHandoverRequested(address)"))`.
    uint256 private constant _OWNERSHIP_HANDOVER_REQUESTED_EVENT_SIGNATURE =
        0xdbf36a107da19e49527a7176a1babf963b4b0ff8cde35ee35d6cd8f1f9ac7e1d;

    /// @dev `keccak256(bytes("OwnershipHandoverCanceled(address)"))`.
    uint256 private constant _OWNERSHIP_HANDOVER_CANCELED_EVENT_SIGNATURE =
        0xfa7b8eab7da67f412cc9575ed43464468f9bfbae89d1675917346ca6d8fe3c92;

    /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
    /*                          STORAGE                           */
    /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

    /// @dev The owner slot is given by:
    /// `bytes32(~uint256(uint32(bytes4(keccak256("_OWNER_SLOT_NOT")))))`.
    /// It is intentionally chosen to be a high value
    /// to avoid collision with lower slots.
    /// The choice of manual storage layout is to enable compatibility
    /// with both regular and upgradeable contracts.
    bytes32 internal constant _OWNER_SLOT =
        0xffffffffffffffffffffffffffffffffffffffffffffffffffffffff74873927;

    /// The ownership handover slot of `newOwner` is given by:
    /// ```
    ///     mstore(0x00, or(shl(96, user), _HANDOVER_SLOT_SEED))
    ///     let handoverSlot := keccak256(0x00, 0x20)
    /// ```
    /// It stores the expiry timestamp of the two-step ownership handover.
    uint256 private constant _HANDOVER_SLOT_SEED = 0x389a75e1;

    /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
    /*                     INTERNAL FUNCTIONS                     */
    /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

    /// @dev Override to return true to make `_initializeOwner` prevent double-initialization.
    function _guardInitializeOwner() internal pure virtual returns (bool guard) {}

    /// @dev Initializes the owner directly without authorization guard.
    /// This function must be called upon initialization,
    /// regardless of whether the contract is upgradeable or not.
    /// This is to enable generalization to both regular and upgradeable contracts,
    /// and to save gas in case the initial owner is not the caller.
    /// For performance reasons, this function will not check if there
    /// is an existing owner.
    function _initializeOwner(address newOwner) internal virtual {
        if (_guardInitializeOwner()) {
            /// @solidity memory-safe-assembly
            assembly {
                let ownerSlot := _OWNER_SLOT
                if sload(ownerSlot) {
                    mstore(0x00, 0x0dc149f0) // `AlreadyInitialized()`.
                    revert(0x1c, 0x04)
                }
                // Clean the upper 96 bits.
                newOwner := shr(96, shl(96, newOwner))
                // Store the new value.
                sstore(ownerSlot, or(newOwner, shl(255, iszero(newOwner))))
                // Emit the {OwnershipTransferred} event.
                log3(0, 0, _OWNERSHIP_TRANSFERRED_EVENT_SIGNATURE, 0, newOwner)
            }
        } else {
            /// @solidity memory-safe-assembly
            assembly {
                // Clean the upper 96 bits.
                newOwner := shr(96, shl(96, newOwner))
                // Store the new value.
                sstore(_OWNER_SLOT, newOwner)
                // Emit the {OwnershipTransferred} event.
                log3(0, 0, _OWNERSHIP_TRANSFERRED_EVENT_SIGNATURE, 0, newOwner)
            }
        }
    }

    /// @dev Sets the owner directly without authorization guard.
    function _setOwner(address newOwner) internal virtual {
        if (_guardInitializeOwner()) {
            /// @solidity memory-safe-assembly
            assembly {
                let ownerSlot := _OWNER_SLOT
                // Clean the upper 96 bits.
                newOwner := shr(96, shl(96, newOwner))
                // Emit the {OwnershipTransferred} event.
                log3(0, 0, _OWNERSHIP_TRANSFERRED_EVENT_SIGNATURE, sload(ownerSlot), newOwner)
                // Store the new value.
                sstore(ownerSlot, or(newOwner, shl(255, iszero(newOwner))))
            }
        } else {
            /// @solidity memory-safe-assembly
            assembly {
                let ownerSlot := _OWNER_SLOT
                // Clean the upper 96 bits.
                newOwner := shr(96, shl(96, newOwner))
                // Emit the {OwnershipTransferred} event.
                log3(0, 0, _OWNERSHIP_TRANSFERRED_EVENT_SIGNATURE, sload(ownerSlot), newOwner)
                // Store the new value.
                sstore(ownerSlot, newOwner)
            }
        }
    }

    /// @dev Throws if the sender is not the owner.
    function _checkOwner() internal view virtual {
        /// @solidity memory-safe-assembly
        assembly {
            // If the caller is not the stored owner, revert.
            if iszero(eq(caller(), sload(_OWNER_SLOT))) {
                mstore(0x00, 0x82b42900) // `Unauthorized()`.
                revert(0x1c, 0x04)
            }
        }
    }

    /// @dev Returns how long a two-step ownership handover is valid for in seconds.
    /// Override to return a different value if needed.
    /// Made internal to conserve bytecode. Wrap it in a public function if needed.
    function _ownershipHandoverValidFor() internal view virtual returns (uint64) {
        return 48 * 3600;
    }

    /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
    /*                  PUBLIC UPDATE FUNCTIONS                   */
    /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

    /// @dev Allows the owner to transfer the ownership to `newOwner`.
    function transferOwnership(address newOwner) public payable virtual onlyOwner {
        /// @solidity memory-safe-assembly
        assembly {
            if iszero(shl(96, newOwner)) {
                mstore(0x00, 0x7448fbae) // `NewOwnerIsZeroAddress()`.
                revert(0x1c, 0x04)
            }
        }
        _setOwner(newOwner);
    }

    /// @dev Allows the owner to renounce their ownership.
    function renounceOwnership() public payable virtual onlyOwner {
        _setOwner(address(0));
    }

    /// @dev Request a two-step ownership handover to the caller.
    /// The request will automatically expire in 48 hours (172800 seconds) by default.
    function requestOwnershipHandover() public payable virtual {
        unchecked {
            uint256 expires = block.timestamp + _ownershipHandoverValidFor();
            /// @solidity memory-safe-assembly
            assembly {
                // Compute and set the handover slot to `expires`.
                mstore(0x0c, _HANDOVER_SLOT_SEED)
                mstore(0x00, caller())
                sstore(keccak256(0x0c, 0x20), expires)
                // Emit the {OwnershipHandoverRequested} event.
                log2(0, 0, _OWNERSHIP_HANDOVER_REQUESTED_EVENT_SIGNATURE, caller())
            }
        }
    }

    /// @dev Cancels the two-step ownership handover to the caller, if any.
    function cancelOwnershipHandover() public payable virtual {
        /// @solidity memory-safe-assembly
        assembly {
            // Compute and set the handover slot to 0.
            mstore(0x0c, _HANDOVER_SLOT_SEED)
            mstore(0x00, caller())
            sstore(keccak256(0x0c, 0x20), 0)
            // Emit the {OwnershipHandoverCanceled} event.
            log2(0, 0, _OWNERSHIP_HANDOVER_CANCELED_EVENT_SIGNATURE, caller())
        }
    }

    /// @dev Allows the owner to complete the two-step ownership handover to `pendingOwner`.
    /// Reverts if there is no existing ownership handover requested by `pendingOwner`.
    function completeOwnershipHandover(address pendingOwner) public payable virtual onlyOwner {
        /// @solidity memory-safe-assembly
        assembly {
            // Compute and set the handover slot to 0.
            mstore(0x0c, _HANDOVER_SLOT_SEED)
            mstore(0x00, pendingOwner)
            let handoverSlot := keccak256(0x0c, 0x20)
            // If the handover does not exist, or has expired.
            if gt(timestamp(), sload(handoverSlot)) {
                mstore(0x00, 0x6f5e8818) // `NoHandoverRequest()`.
                revert(0x1c, 0x04)
            }
            // Set the handover slot to 0.
            sstore(handoverSlot, 0)
        }
        _setOwner(pendingOwner);
    }

    /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
    /*                   PUBLIC READ FUNCTIONS                    */
    /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

    /// @dev Returns the owner of the contract.
    function owner() public view virtual returns (address result) {
        /// @solidity memory-safe-assembly
        assembly {
            result := sload(_OWNER_SLOT)
        }
    }

    /// @dev Returns the expiry timestamp for the two-step ownership handover to `pendingOwner`.
    function ownershipHandoverExpiresAt(address pendingOwner)
        public
        view
        virtual
        returns (uint256 result)
    {
        /// @solidity memory-safe-assembly
        assembly {
            // Compute the handover slot.
            mstore(0x0c, _HANDOVER_SLOT_SEED)
            mstore(0x00, pendingOwner)
            // Load the handover slot.
            result := sload(keccak256(0x0c, 0x20))
        }
    }

    /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
    /*                         MODIFIERS                          */
    /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

    /// @dev Marks a function as only callable by the owner.
    modifier onlyOwner() virtual {
        _checkOwner();
        _;
    }
}
"
    },
    "lib/solady/src/utils/ReentrancyGuard.sol": {
      "content": "// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

/// @notice Reentrancy guard mixin.
/// @author Solady (https://github.com/vectorized/solady/blob/main/src/utils/ReentrancyGuard.sol)
abstract contract ReentrancyGuard {
    /*´:°•.°+.*•´.*:˚.°*.˚•

Tags:
ERC20, ERC165, Multisig, Mintable, Burnable, Swap, Liquidity, Upgradeable, Multi-Signature, Factory, Oracle|addr:0xb395eea1c4344ac6b8e2cd6310615368fc6dc27d|verified:true|block:23462521|tx:0x8ace08b5a976e4be395e2354e53677effcdfba4af35bbd56db314e415ea05fcb|first_check:1759084188

Submitted on: 2025-09-28 20:29:48

Comments

Log in to comment.

No comments yet.