PerpetualFactory

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/PerpetualFactory.sol": {
      "content": "// SPDX-License-Identifier: MIT
pragma solidity ^0.8.21;

/*
██████╗ ███████╗██████╗ ██████╗ ███████╗████████╗██╗   ██╗ █████╗ ██╗
██╔══██╗██╔════╝██╔══██╗██╔══██╗██╔════╝╚══██╔══╝██║   ██║██╔══██╗██║
██████╔╝█████╗  ██████╔╝██████╔╝█████╗     ██║   ██║   ██║███████║██║
██╔═══╝ ██╔══╝  ██╔══██╗██╔═══╝ ██╔══╝     ██║   ██║   ██║██╔══██║██║
██║     ███████╗██║  ██║██║     ███████╗   ██║   ╚██████╔╝██║  ██║███████╗
╚═╝     ╚══════╝╚═╝  ╚═╝╚═╝     ╚══════╝   ╚═╝    ╚═════╝ ╚═╝  ╚═╝╚══════╝

███╗   ███╗███████╗███╗   ███╗███████╗
████╗ ████║██╔════╝████╗ ████║██╔════╝
██╔████╔██║█████╗  ██╔████╔██║█████╗
██║╚██╔╝██║██╔══╝  ██║╚██╔╝██║██╔══╝
██║ ╚═╝ ██║███████╗██║ ╚═╝ ██║███████╗
╚═╝     ╚═╝╚══════╝╚═╝     ╚═╝╚══════╝

https://perpetual.meme/
*/

import {Ownable} from "solady/auth/Ownable.sol";
import {PerpetualMeme} from "./PerpetualMeme.sol";
import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol";
import {IPositionManager} from "@uniswap/v4-periphery/src/interfaces/IPositionManager.sol";
import {IAllowanceTransfer} from "permit2/src/interfaces/IAllowanceTransfer.sol";
import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol";
import {Currency} from "@uniswap/v4-core/src/types/Currency.sol";
import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
import {Actions} from "@uniswap/v4-periphery/src/libraries/Actions.sol";
import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol";
import {IUniswapV4Router04} from "v4-router/interfaces/IUniswapV4Router04.sol";
import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
import "./Interfaces.sol";
import {ReentrancyGuard} from "solady/utils/ReentrancyGuard.sol";
import {LibClone} from "solady/utils/LibClone.sol";

/// @title PerpetualFactory
contract PerpetualFactory is Ownable, ReentrancyGuard {
    /* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ */
    /*                      CONSTANTS                      */
    /* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ */

    /// @notice ETH amount used for initial liquidity pairing
    uint256 private constant ethToPair = 2 wei;
    /// @notice Uniswap V4 Position Manager for liquidity operations
    IPositionManager private immutable posm;
    /// @notice Permit2 contract for token approvals
    IAllowanceTransfer private immutable permit2;
    /// @notice Uniswap V4 Router for token swaps
    IUniswapV4Router04 private immutable router;
    /// @notice Uniswap V4 Pool Manager for pool operations
    IPoolManager private immutable poolManager;
    /// @notice Universal Router for pool operations
    address private immutable universalRouter;
    /// @notice Dead address for burning tokens
    address public constant DEAD_ADDRESS = 0x000000000000000000000000000000000000dEaD;

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

    /// @notice Mapping of contract addresses to their PerpetualMeme contract
    mapping(address => address) public tokenMemeToPerpetualMeme;
    /// @notice Mapping of PerpetualMeme addresses to their token contract
    mapping(address => address) public perpetualMemeToTokenMeme;
    /// @notice The Uniswap V4 hook that control the logic of new deploys
    address public hookAddress;
    /// @notice The address to send fees to
    address public feeAddress;
    /// @notice Gate the PerpetualHook to only when we're loading a new token
    bool public loadingLiquidity;
    /// @notice list of valid routers
    mapping(address => bool) public listOfRouters;

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

    /// @notice Hook address has not been set
    error HookNotSet();
    /// @notice Token meme has already been launched
    error TokenMemeAlreadyLaunched();
    /// @notice Incorrect ETH amount sent with launch transaction
    error WrongEthAmount();

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

    /// @notice Event emitted when a new PerpetualMeme instance is launched
    event PerpetualMemeLaunched(
        address indexed tokenMeme, address indexed perpetualMeme, string tokenName, string tokenSymbol
    );

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

    constructor(address _posm, address _permit2, address _poolManager, address payable _router, address _feeAddress) {
        posm = IPositionManager(_posm);
        permit2 = IAllowanceTransfer(_permit2);
        poolManager = IPoolManager(_poolManager);
        router = IUniswapV4Router04(_router);
        feeAddress = _feeAddress;

        _initializeOwner(msg.sender);
    }

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

    /// @notice Updates the hook attached to new PerpetualMeme pools
    /// @param _hookAddress New Uniswap v4 hook address
    /// @dev Only callable by owner
    function updateHookAddress(address _hookAddress) external onlyOwner {
        hookAddress = _hookAddress;
    }

    /// @notice Updates the name of a specific PerpetualMeme token
    /// @param perpetualMeme Address of the PerpetualMeme contract
    /// @param tokenName New name for the token
    function updateTokenName(address perpetualMeme, string memory tokenName) external onlyOwner {
        IPerpetualMeme(perpetualMeme).updateName(tokenName);
    }

    /// @notice Updates the symbol of a specific PerpetualMeme token
    /// @param perpetualMeme Address of the PerpetualMeme contract
    /// @param tokenSymbol New symbol for the token
    function updateTokenSymbol(address perpetualMeme, string memory tokenSymbol) external onlyOwner {
        IPerpetualMeme(perpetualMeme).updateSymbol(tokenSymbol);
    }

    /// @notice Updates the price multiplier for a specific PerpetualMeme
    /// @param perpetualMeme Address of the PerpetualMeme contract
    /// @param newMultiplier New multiplier in basis points (1100 = 1.1x)
    function updatePriceMultiplier(address perpetualMeme, uint256 newMultiplier) external onlyOwner {
        IPerpetualMeme(perpetualMeme).setPriceMultiplier(newMultiplier);
    }

    /// @notice Updates the hook address of a specific PerpetualMeme
    /// @param perpetualMeme Address of the PerpetualMeme contract
    /// @param _hookAddress New hook address
    function updateHookAddressOfPerpetualMeme(address perpetualMeme, address _hookAddress) external onlyOwner {
        IPerpetualMeme(perpetualMeme).updateHookAddress(_hookAddress);
    }

    /// @notice Allows owner to whitelist addresses that can distribute tokens freely for a specific PerpetualMeme
    /// @param perpetualMeme Address of the PerpetualMeme contract
    /// @param distributor Address to whitelist
    /// @param status True to whitelist, false to remove from whitelist
    function setDistributorOfPerpetualMeme(address perpetualMeme, address distributor, bool status) external onlyOwner {
        IPerpetualMeme(perpetualMeme).setDistributor(distributor, status);
    }

    /// @notice Updates the default minimum tokens to buy for a specific PerpetualMeme
    /// @param perpetualMeme Address of the PerpetualMeme contract
    /// @param _defaultMinimumTokens New default minimum tokens to buy
    function setDefaultMinimumTokensOfPerpetualMeme(address perpetualMeme, uint256 _defaultMinimumTokens)
        external
        onlyOwner
    {
        IPerpetualMeme(perpetualMeme).setDefaultMinimumTokens(_defaultMinimumTokens);
    }

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

    /// @notice Internal function to load liquidity into the Uniswap V4 pool
    /// @param _token Address of the PerpetualMeme ERC20
    function _loadLiquidity(address _token) internal {
        loadingLiquidity = true;

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

        uint24 lpFee = 0;
        int24 tickSpacing = 60;

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

        // 10e18 ETH = 1_000_000_000e18 TOKEN
        uint160 startingPrice = 501082896750095888663770159906816;

        int24 tickLower = TickMath.minUsableTick(tickSpacing);
        int24 tickUpper = int24(175020);

        PoolKey memory key = PoolKey(currency0, currency1, lpFee, tickSpacing, IHooks(hookAddress));
        bytes memory hookData = new bytes(0);

        // Hardcoded from LiquidityAmounts.getLiquidityForAmounts
        uint128 liquidity = 158372218983990412488087;

        uint256 amount0Max = token0Amount + 1 wei;
        uint256 amount1Max = token1Amount + 1 wei;

        (bytes memory actions, bytes[] memory mintParams) =
            _mintLiquidityParams(key, tickLower, tickUpper, liquidity, amount0Max, amount1Max, DEAD_ADDRESS, 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 = amount0Max;
        permit2.approve(_token, address(posm), type(uint160).max, type(uint48).max);

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

        loadingLiquidity = false;
    }

    /// @notice Creates parameters for minting liquidity in Uniswap V4
    /// @param poolKey The pool key for the liquidity position
    /// @param _tickLower Lower tick boundary
    /// @param _tickUpper Upper tick boundary
    /// @param liquidity Amount of liquidity to mint
    /// @param amount0Max Maximum amount of token0 to use
    /// @param amount1Max Maximum amount of token1 to use
    /// @param recipient Address to receive the liquidity position
    /// @param hookData Additional data for hooks
    /// @return Encoded actions and parameters for position manager
    /// @dev Internal helper for liquidity operations
    function _mintLiquidityParams(
        PoolKey memory poolKey,
        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(poolKey, _tickLower, _tickUpper, liquidity, amount0Max, amount1Max, recipient, hookData);
        params[1] = abi.encode(poolKey.currency0, poolKey.currency1);
        return (actions, params);
    }

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

    /// @notice Launches a new PerpetualMeme contract for a token meme with owner permissions
    /// @param tokenMeme Address of the token meme contract
    /// @param tokenName Name of the strategy token
    /// @param tokenSymbol Symbol of the strategy token
    /// @dev Only callable by contract owner. Deploys new PerpetualMeme and initializes liquidity
    function launchPerpetualMeme(address tokenMeme, string memory tokenName, string memory tokenSymbol)
        external
        payable
        onlyOwner
        returns (PerpetualMeme)
    {
        // Validate the parameters passed
        if (hookAddress == address(0)) revert HookNotSet();
        if (tokenMemeToPerpetualMeme[tokenMeme] != address(0)) revert TokenMemeAlreadyLaunched();

        PerpetualMeme perpetualMeme =
            new PerpetualMeme(address(this), hookAddress, router, poolManager, tokenMeme, tokenName, tokenSymbol);

        tokenMemeToPerpetualMeme[tokenMeme] = address(perpetualMeme);
        perpetualMemeToTokenMeme[address(perpetualMeme)] = tokenMeme;

        // Costs 2 wei
        _loadLiquidity(address(perpetualMeme));

        emit PerpetualMemeLaunched(tokenMeme, address(perpetualMeme), tokenName, tokenSymbol);

        return perpetualMeme;
    }

    /// @notice Checks if a token meme already has a perpetual launched
    /// @param tokenMeme The address of the Token Meme to check
    /// @return True if token meme already has a perpetual launched, false otherwise
    function checkIfAlreadyLaunched(address tokenMeme) public view returns (bool) {
        return tokenMemeToPerpetualMeme[tokenMeme] != address(0);
    }

    /// @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();
        _;
    }
}
"
    },
    "src/PerpetualMeme.sol": {
      "content": "// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

/*
██████╗ ███████╗██████╗ ██████╗ ███████╗████████╗██╗   ██╗ █████╗ ██╗
██╔══██╗██╔════╝██╔══██╗██╔══██╗██╔════╝╚══██╔══╝██║   ██║██╔══██╗██║
██████╔╝█████╗  ██████╔╝██████╔╝█████╗     ██║   ██║   ██║███████║██║
██╔═══╝ ██╔══╝  ██╔══██╗██╔═══╝ ██╔══╝     ██║   ██║   ██║██╔══██║██║
██║     ███████╗██║  ██║██║     ███████╗   ██║   ╚██████╔╝██║  ██║███████╗
╚═╝     ╚══════╝╚═╝  ╚═╝╚═╝     ╚══════╝   ╚═╝    ╚═════╝ ╚═╝  ╚═╝╚══════╝

███╗   ███╗███████╗███╗   ███╗███████╗
████╗ ████║██╔════╝████╗ ████║██╔════╝
██╔████╔██║█████╗  ██╔████╔██║█████╗
██║╚██╔╝██║██╔══╝  ██║╚██╔╝██║██╔══╝
██║ ╚═╝ ██║███████╗██║ ╚═╝ ██║███████╗
╚═╝     ╚═╝╚══════╝╚═╝     ╚═╝╚══════╝

https://perpetual.meme/
*/

import {ERC20} from "solady/tokens/ERC20.sol";
import {ReentrancyGuard} from "solady/utils/ReentrancyGuard.sol";
import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol";
import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol";
import {Currency} from "@uniswap/v4-core/src/types/Currency.sol";
import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
import {IUniswapV4Router04} from "v4-router/interfaces/IUniswapV4Router04.sol";
import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
import "./Interfaces.sol";

/// @title PerpetualMeme
contract PerpetualMeme is ERC20, ReentrancyGuard {
    /* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ */
    /*                      CONSTANTS                      */
    /* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ */
    /// @notice The address of the Uniswap V4 router
    IUniswapV4Router04 private immutable router;
    /// @notice The address of the Uniswap V4 Pool Manager
    IPoolManager private immutable poolManager;
    /// @notice The name of the ERC20 token
    string tokenName;
    /// @notice The symbol of the ERC20 token
    string tokenSymbol;
    /// @notice The address of the Uniswap V4 hook
    address public hookAddress;
    /// @notice The address of the PerpetualFactory contract
    address public immutable factory;
    /// @notice The address of the ERC20 Token Meme
    IERC20 public immutable tokenMeme;
    /// @notice The maximum supply of the ERC20 token
    uint256 public constant MAX_SUPPLY = 1_000_000_000 * 1e18;
    /// @notice The dead address for burning tokens
    address public constant DEADADDRESS = 0x000000000000000000000000000000000000dEaD;

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

    /// @notice The price multiplier for take profits (in basis points, e.g., 1200 = 1.2x)
    uint256 public priceMultiplier;
    /// @notice The current fees in ETH
    uint256 public currentFees;
    /// @notice The amount of ETH to TWAP increment for meme
    uint256 public twapMemeIncrement;
    /// @notice The delay in blocks for TWAP Meme
    uint256 public twapMemeDelayInBlocks;
    /// @notice The last TWAP Meme block
    uint256 public lastTwapMemeBlock;
    /// @notice The start block of the Dutch Auction
    uint256 public dutchAuctionStartBlock;
    /// @notice Mapping of addresses that can distribute tokens freely (team wallets, airdrop contracts)
    mapping(address => bool) public isDistributor;
    /// @notice The default minimum tokens to buy
    uint256 public defaultMinimumTokens;

    /// @notice Struct to store position information
    struct PositionInfo {
        uint256 amountSpent; // Total ETH spent on the position
        uint256 tokensBought; // Amount of Token Meme bought on the position
        uint256 openTimestamp; // When the position was opened
        uint256 closeTimestamp; // When the position was closed
        uint256 amountProfit; // Profit earned on the position in ETH
    }

    /// @notice Mapping from position ID to position info
    mapping(uint256 positionId => PositionInfo positionInfo) public positions;

    /// @notice The next position ID to be used
    uint256 public nextPositionId;

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

    /// @notice Emitted when a position is open
    event PositionOpen(uint256 indexed positionId, uint256 buyCost, uint256 tokensBought);
    /// @notice Emitted when a position is sold
    event PositionSold(uint256 indexed positionId, uint256 buyCost, uint256 sellCost);
    /// @notice Emitted when tokens are cleared from the contract
    event TokenCleared(address indexed tokenAddress, uint256 tokens);
    /// @notice Emitted when ETH is cleared from the contract
    event ETHCleared(uint256 ethAmount);
    /// @notice Emitted when transfer allowance is increased by the hook
    event AllowanceIncreased(uint256 amount);
    /// @notice Emitted when transfer allowance is spent
    event AllowanceSpent(address indexed from, address indexed to, uint256 amount);
    /// @notice Emitted when a distributor's whitelist status is updated
    event DistributorUpdated(address indexed distributor, bool status);

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

    /// @notice Contract doesn't have enough ETH balance to buy tokens
    error InsufficientContractBalance();
    /// @notice Invalid price multiplier
    error InvalidMultiplier();
    /// @notice No ETH to TWAP Meme
    error NoETHToTwapMeme();
    /// @notice TWAP delay not met
    error TwapDelayNotMet();
    /// @notice TWAP Meme delay not met
    error TwapMemeDelayNotMet();
    /// @notice Not enough ETH
    error NotEnoughEth();
    /// @notice Not factory
    error NotFactory();
    /// @notice Not Perpetual Factory Owner
    error NotPerpetualFactoryOwner();
    /// @notice Only hook
    error OnlyHook();
    /// @notice Not valid router
    error NotValidRouter();
    /// @notice Need to buy token meme
    error PriceTooHigh();
    /// @notice Position already sold
    error PositionAlreadySold();
    /// @notice Price too low
    error PriceTooLow();
    /// @notice Token transfer failed
    error TokenTransferFailed();
    /// @notice No tokens meme to clear
    error NoTokensMemeToClear();
    /// @notice No ETH to clear
    error NoETHToClear();
    /// @notice External call failed
    error ExternalCallFailed(bytes reason);
    /// @notice Invalid target address for external call
    error InvalidTarget();
    /// @notice Token transfer not authorized
    error InvalidTransfer();

    /* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ */
    /*                     CONSTRUCTOR                     */
    /* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ */
    /// @notice Initializes the contract with required addresses and permissions
    /// @param _factory Address of the PerpetualFactory contract
    /// @param _hook Address of the PerpetualHook contract
    /// @param _router Address of the Uniswap V4 Router contract
    /// @param _tokenMeme Address of the token meme contract
    /// @param _tokenName Name of the token
    /// @param _tokenSymbol Symbol of the token
    constructor(
        address _factory,
        address _hook,
        IUniswapV4Router04 _router,
        IPoolManager _poolManager,
        address _tokenMeme,
        string memory _tokenName,
        string memory _tokenSymbol
    ) {
        require(_tokenMeme != address(0), "Invalid token meme");
        require(bytes(_tokenName).length > 0, "Empty name");
        require(bytes(_tokenSymbol).length > 0, "Empty symbol");

        factory = _factory;
        router = _router;
        poolManager = _poolManager;
        hookAddress = _hook;
        tokenMeme = IERC20(_tokenMeme);
        tokenName = _tokenName;
        tokenSymbol = _tokenSymbol;

        // Initialize default states variables
        priceMultiplier = 1200; // 1.2x
        twapMemeIncrement = 1 ether;
        twapMemeDelayInBlocks = 1;
        defaultMinimumTokens = tokenMeme.totalSupply() / 100; // Default to 1% of the total supply

        _mint(factory, MAX_SUPPLY);
    }

    /* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ */
    /*                     MODIFIERS                       */
    /* ™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™™ */

    /// @notice Modifier to restrict function access to the factory contract only
    modifier onlyFactory() {
        _onlyFactory();
        _;
    }

    /// @notice Internal function to check if the caller is the factory
    /// @dev Reverts if the caller is not the factory
    function _onlyFactory() internal view {
        if (msg.sender != factory) revert NotFactory();
    }

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

    /// @notice Returns the name of the token
    /// @return The token name as a string
    function name() public view override returns (string memory) {
        return tokenName;
    }

    /// @notice Returns the symbol of the token
    /// @return The token symbol as a string
    function symbol() public view override returns (string memory) {
        return tokenSymbol;
    }

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

    /// @notice Updates the name of the token
    /// @dev Can only be called by the factory
    /// @param _tokenName New name for the token
    function updateName(string memory _tokenName) external onlyFactory {
        tokenName = _tokenName;
    }

    /// @notice Updates the symbol of the token
    /// @dev Can only be called by the factory
    /// @param _tokenSymbol New symbol for the token
    function updateSymbol(string memory _tokenSymbol) external onlyFactory {
        tokenSymbol = _tokenSymbol;
    }

    /// @notice Updates the hook address
    /// @dev Can only be called by the owner
    /// @param _hookAddress New hook address
    function updateHookAddress(address _hookAddress) external onlyFactory {
        hookAddress = _hookAddress;
    }

    /// @notice Updates the price multiplier for take profits
    /// @param _newMultiplier New multiplier in basis points (1100 = 1.1x, 10000 = 10.0x)
    /// @dev Only callable by factory. Must be between 1.1x (1100) and 10.0x (10000)
    function setPriceMultiplier(uint256 _newMultiplier) external onlyFactory {
        if (_newMultiplier < 1100 || _newMultiplier > 10000) revert InvalidMultiplier();
        priceMultiplier = _newMultiplier;
    }

    /// @notice Allows owner to whitelist addresses that can distribute tokens freely
    /// @param distributor Address to whitelist
    /// @param status True to whitelist, false to remove from whitelist
    /// @dev Only callable by factory. Enables fee-free token distribution for whitelisted addresses
    function setDistributor(address distributor, bool status) external onlyFactory {
        isDistributor[distributor] = status;
        emit DistributorUpdated(distributor, status);
    }

    /// @notice Updates the default minimum tokens to buy
    /// @param _defaultMinimumTokens New default minimum tokens to buy
    /// @dev Only callable by factory. Must be greater than 0 and less than the total supply
    function setDefaultMinimumTokens(uint256 _defaultMinimumTokens) external onlyFactory {
        require(
            _defaultMinimumTokens > 0 && _defaultMinimumTokens < tokenMeme.totalSupply(),
            "Invalid default minimum tokens"
        );
        defaultMinimumTokens = _defaultMinimumTokens;
    }

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

    /// @notice Allows the hook to deposit trading fees into the contract
    /// @dev Only callable by the hook contract
    /// @dev Fees are added to currentFees balance
    function addFees() external payable {
        if (msg.sender != hookAddress) revert OnlyHook();
        currentFees += msg.value;

        // Start Dutch Auction if not already started and enough ETH to TWAPMeme
        if (dutchAuctionStartBlock == 0 && currentFees >= twapMemeIncrement) {
            dutchAuctionStartBlock = block.number;
        }
    }

    /// @notice Increases the transient transfer allowance for pool operations
    /// @param amountAllowed Amount to add to the current allowance
    /// @dev Only callable by the hook contract, uses transient storage
    function increaseTransferAllowance(uint256 amountAllowed) external {
        if (msg.sender != hookAddress) revert OnlyHook();
        uint256 currentAllowance = getTransferAllowance();
        assembly {
            tstore(0, add(currentAllowance, amountAllowed))
        }
        emit AllowanceIncreased(amountAllowed);
    }

    /// @notice Gets the minimum amount of tokens that must be bought
    /// @return The minimum amount of tokens that must be bought
    function getMinimumTokensToBuy() public view returns (uint256) {
        uint256 minimumTokens;

        if (block.number < lastTwapMemeBlock + 5) {
            // Less than 5 blocks since last position buy, 1.05x the last tokens bought
            minimumTokens = positions[nextPositionId - 1].tokensBought * 105 / 100;
        } else if (nextPositionId > 1) {
            // More than 5 blocks since last position buy, 2x the last tokens bought
            minimumTokens = positions[nextPositionId - 1].tokensBought * 2;
        } else {
            // No positions have been opened yet, use default minimum tokens
            minimumTokens = defaultMinimumTokens;
        }

        // Blocks since start of Dutch Auction
        uint256 blocksPassed = block.number - dutchAuctionStartBlock;

        // Assuming exponential decrease of tokens bought over time 0.5% per block
        for (uint256 i = 0; i < blocksPassed; i++) {
            minimumTokens = minimumTokens * 995 / 1000;
        }

        return minimumTokens;
    }

    /// @notice Opens a position and buys tokens
    /// @param data The data to pass to the target
    /// @param target The target to call
    /// @dev Only callable by the factory owner
    function openPosition(bytes calldata data, address target) external nonReentrant {
        // Check if Dutch Auction has started
        require(dutchAuctionStartBlock > 0, "Dutch Auction not started");

        // Check if there is enough ETH to buy tokens
        if (currentFees < twapMemeIncrement) revert NoETHToTwapMeme();

        // Check if enough blocks have passed since last TWAP Meme
        if (block.number < lastTwapMemeBlock + twapMemeDelayInBlocks) revert TwapMemeDelayNotMet();

        // Store eth balance and token meme balance before calling external
        uint256 ethBalanceBefore = address(this).balance;
        uint256 tokenMemeBalanceBefore = tokenMeme.balanceOf(address(this));

        // Call external
        (bool success, bytes memory reason) = target.call{value: twapMemeIncrement}(data);
        if (!success) revert ExternalCallFailed(reason);

        // Calculate actual tokens bought
        uint256 tokensBought = tokenMeme.balanceOf(address(this)) - tokenMemeBalanceBefore;

        // Check if we bought enough tokens
        if (tokensBought < getMinimumTokensToBuy()) revert PriceTooHigh();

        // Calculate actual cost of the tokens bought
        uint256 cost = ethBalanceBefore - address(this).balance;
        currentFees -= cost;

        // Create a new position
        positions[nextPositionId] = PositionInfo({
            amountSpent: cost,
            tokensBought: tokensBought,
            openTimestamp: block.timestamp,
            closeTimestamp: 0,
            amountProfit: 0
        });
        nextPositionId++;

        // Update last twap meme block
        lastTwapMemeBlock = block.number;

        // Update dutch auction start block
        if (currentFees < twapMemeIncrement) {
            // Set ducth auction to zero if no more ETH to TWAPMeme
            dutchAuctionStartBlock = 0;
        } else {
            // Set dutch auction to current block if enough ETH to TWAPMeme
            dutchAuctionStartBlock = block.number;
        }

        emit PositionOpen(nextPositionId - 1, cost, tokensBought);
    }

    /// @notice Sells a position and adds ETH to TWAP
    /// @param positionId The ID of the position to sell
    /// @param data The data to pass to the target
    /// @param target The target to call
    function sellPosition(uint256 positionId, bytes calldata data, address target) external nonReentrant {
        // Check if position has already been sold
        if (positions[positionId].closeTimestamp != 0) revert PositionAlreadySold();

        // Check if enough blocks have passed since last TWAP Meme
        if (block.number < lastTwapMemeBlock + twapMemeDelayInBlocks) revert TwapMemeDelayNotMet();

        // Get sale price
        uint256 sellCost = positions[positionId].amountSpent * priceMultiplier / 1000;

        // Store eth balance and token meme balance before calling external
        uint256 ethBalanceBefore = address(this).balance;
        uint256 tokenMemeBalanceBefore = tokenMeme.balanceOf(address(this));

        // Approve tokens for target
        SafeTransferLib.safeApprove(address(tokenMeme), target, positions[positionId].tokensBought);

        // Call external
        (bool success, bytes memory reason) = target.call(data);
        if (!success) revert ExternalCallFailed(reason);

        // Calculate actual tokens sold
        uint256 tokensSold = tokenMemeBalanceBefore - tokenMeme.balanceOf(address(this));

        // Calculate actual profit
        uint256 ethReceived = address(this).balance - ethBalanceBefore;

        // Verify sent ETH is more than cost to sell and tokens sold is less than or equal to the tokens bought
        if (ethReceived < sellCost || tokensSold > positions[positionId].tokensBought) revert PriceTooLow();

        // Update position info
        positions[positionId].amountProfit = sellCost - positions[positionId].amountSpent;
        positions[positionId].closeTimestamp = block.timestamp;

        // Buy and burn perpetual meme tokens
        _buyAndBurnTokens(sellCost);

        // Update last twap meme block
        lastTwapMemeBlock = block.number;

        emit PositionSold(positionId, positions[positionId].amountSpent, sellCost);
    }

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

    /// @notice Buys tokens with ETH and burns them by sending to dead address
    /// @param amountIn The amount of ETH to spend on tokens that will be burned
    function _buyAndBurnTokens(uint256 amountIn) internal {
        PoolKey memory key =
            PoolKey(Currency.wrap(address(0)), Currency.wrap(address(this)), 0, 60, IHooks(hookAddress));

        router.swapExactTokensForTokens{value: amountIn}(amountIn, 0, true, key, "", DEADADDRESS, block.timestamp);
    }

    /// @notice Validates token transfers using a transient allowance system
    /// @param from The address sending tokens
    /// @param to The address receiving tokens
    /// @param amount The amount of tokens being transferred
    /// @dev Reverts if transfer isn't through the hook
    function _afterTokenTransfer(address from, address to, uint256 amount) internal override {
        // On perpetual meme launch, we need to allow for supply mint transfer
        if (from == address(0)) {
            return;
        }

        // Allow whitelisted distributors to send tokens freely
        if (isDistributor[from]) {
            return;
        }

        // Transfers to and from the poolManager require a transient allowance thats set by the hook
        if ((from == address(poolManager) || to == address(poolManager))) {
            uint256 transferAllowance = getTransferAllowance();
            require(transferAllowance >= amount, InvalidTransfer());
            assembly {
                let newAllowance := sub(transferAllowance, amount)
                tstore(0, newAllowance)
            }
            emit AllowanceSpent(from, to, amount);
            return;
        }
        revert InvalidTransfer();
    }

    /// @notice Gets the current transient transfer allowance
    /// @return transferAllowance The current allowance amount
    /// @dev Reads from transient storage slot 0
    function getTransferAllowance() public view returns (uint256 transferAllowance) {
        assembly {
            transferAllowance := tload(0)
        }
    }

    /// @notice Allows the contract to receive ETH
    receive() external payable {}

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

    function clearStuckToken(address tokenAddress, uint256 tokens) external returns (bool success) {
        // Only called by PerpetualFactory owner
        if (msg.sender != IPerpetualFactory(factory).owner()) revert NotPerpetualFactoryOwner();

        // If tokens is 0, get the balance of the token
        if (tokens == 0) {
            tokens = IERC20(tokenAddress).balanceOf(address(this));
        }

        // Ensure not trying to clear tokenMeme with positions opened
        if (tokenAddress == address(tokenMeme)) {
            uint256 totalTokensMeme = IERC20(tokenMeme).balanceOf(address(this));

            // Reduce total tokens meme by the amount of tokens bought in positions not closed
            for (uint256 i = 1; i < nextPositionId; i++) {
                if (positions[i].closeTimestamp == 0) {
                    totalTokensMeme -= positions[i].tokensBought;
                }
            }

            // Check if there are any tokens to clear
            if (totalTokensMeme > 0) {
                tokens = totalTokensMeme;
            } else {
                revert NoTokensMemeToClear();
            }
        }

        // Get fee address from perpetual hook
        address receiverAddress = IPerpetualHook(hookAddress).feeAddress();

        // Transfer tokens to receiver address
        emit TokenCleared(tokenAddress, tokens);
        return IERC20(tokenAddress).transfer(receiverAddress, tokens);
    }

    /// @notice Clears stuck ETH from the contract
    /// @dev Only callable by PerpetualFactory owner
    function clearStuckETH() external {
        // Only called by PerpetualFactory owner
        if (msg.sender != IPerpetualFactory(factory).owner()) revert NotPerpetualFactoryOwner();

        // Get eth balance (excluding fees)
        uint256 ethBalance = address(this).balance - currentFees;

        // Check if there is any ETH to clear
        if (ethBalance <= 0) revert NoETHToClear();

        // Get fee address from perpetual hook
        address payable receiverAddress = payable(IPerpetualHook(hookAddress).feeAddress());

        // Transfer ETH to receiver address
        emit ETHCleared(ethBalance);
        receiverAddress.transfer(ethBalance);
    }
}
"
    },
    "lib/solady/src/utils/SafeTransferLib.sol": {
      "content": "// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

/// @notice Safe ETH and ERC20 transfer library that gracefully handles missing return values.
/// @author Solady (https://github.com/vectorized/solady/blob/main/src/utils/SafeTransferLib.sol)
/// @author Modified from Solmate (https://github.com/transmissions11/solmate/blob/main/src/utils/SafeTransferLib.sol)
/// @author Permit2 operations from (https://github.com/Uniswap/permit2/blob/main/src/libraries/Permit2Lib.sol)
///
/// @dev Note:
/// - For ETH transfers, please use `forceSafeTransferETH` for DoS protection.
library SafeTransferLib {
    /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
    /*                       CUSTOM ERRORS                        */
    /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

    /// @dev The ETH transfer has failed.
    error ETHTransferFailed();

    /// @dev The ERC20 `transferFrom` has failed.
    error TransferFromFailed();

    /// @dev The ERC20 `transfer` has failed.
    error TransferFailed();

    /// @dev The ERC20 `approve` has failed.
    error ApproveFailed();

    /// @dev The ERC20 `totalSupply` query has failed.
    error TotalSupplyQueryFailed();

    /// @dev The Permit2 operation has failed.
    error Permit2Failed();

    /// @dev The Permit2 amount must be less than `2**160 - 1`.
    error Permit2AmountOverflow();

    /// @dev The Permit2 approve operation has failed.
    error Permit2ApproveFailed();

    /// @dev The Permit2 lockdown operation has failed.
    error Permit2LockdownFailed();

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

    /// @dev Suggested gas stipend for contract receiving ETH that disallows any storage writes.
    uint256 internal constant GAS_STIPEND_NO_STORAGE_WRITES = 2300;

    /// @dev Suggested gas stipend for contract receiving ETH to perform a few
    /// storage reads and writes, but low enough to prevent griefing.
    uint256 internal constant GAS_STIPEND_NO_GRIEF = 100000;

    /// @dev The unique EIP-712 domain separator for the DAI token contract.
    bytes32 internal constant DAI_DOMAIN_SEPARATOR =
        0xdbb8cf42e1ecb028be3f3dbc922e1d878b963f411dc388ced501601c60f7c6f7;

    /// @dev The address for the WETH9 contract on Ethereum mainnet.
    address internal constant WETH9 = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;

    /// @dev The canonical Permit2 address.
    /// [Github](https://github.com/Uniswap/permit2)
    /// [Etherscan](https://etherscan.io/address/0x000000000022D473030F116dDEE9F6B43aC78BA3)
    address internal constant PERMIT2 = 0x000000000022D473030F116dDEE9F6B43aC78BA3;

    /// @dev The canonical address of the `SELFDESTRUCT` ETH mover.
    /// See: https://gist.github.com/Vectorized/1cb8ad4cf393b1378e08f23f79bd99fa
    /// [Etherscan](https://etherscan.io/address/0x00000000000073c48c8055bD43D1A53799176f0D)
    address internal constant ETH_MOVER = 0x00000000000073c48c8055bD43D1A53799176f0D;

    /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
    /*                       ETH OPERATIONS                       */
    /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

    // If the ETH transfer MUST succeed with a reasonable gas budget, use the force variants.
    //
    // The regular variants:
    // - Forwards all remaining gas to the target.
    // - Reverts if the target reverts.
    // - Reverts if the current contract has insufficient balance.
    //
    // The force variants:
    // - Forwards with an optional gas stipend
    //   (defaults to `GAS_STIPEND_NO_GRIEF`, which is sufficient for most cases).
    // - If the target reverts, or if the gas stipend is exhausted,
    //   creates a temporary contract to force send the ETH via `SELFDESTRUCT`.
    //   Future compatible with `SENDALL`: https://eips.ethereum.org/EIPS/eip-4758.
    // - Reverts if the current contract has insufficient balance.
    //
    // The try variants:
    // - Forwards with a mandatory gas stipend.
    // - Instead of reverting, returns whether the transfer succeeded.

    /// @dev Sends `amount` (in wei) ETH to `to`.
    function safeTransferETH(address to, uint256 amount) internal {
        /// @solidity memory-safe-assembly
        assembly {
            if iszero(call(gas(), to, amount, codesize(), 0x00, codesize(), 0x00)) {
                mstore(0x00, 0xb12d13eb) // `ETHTransferFailed()`.
                revert(0x1c, 0x04)
            }
        }
    }

    /// @dev Sends all the ETH in the current contract to `to`.
    function safeTransferAllETH(address to) internal {
        /// @solidity memory-safe-assembly
        assembly {
            // Transfer all the ETH and check if it succeeded or not.
            if iszero(call(gas(), to, selfbalance(), codesize(), 0x00, codesize(), 0x00)) {
                mstore(0x00, 0xb12d13eb) // `ETHTransferFailed()`.
                revert(0x1c, 0x04)
            }
        }
    }

    /// @dev Force sends `amount` (in wei) ETH to `to`, with a `gasStipend`.
    function forceSafeTransferETH(address to, uint256 amount, uint256 gasStipend) internal {
        /// @solidity memory-safe-assembly
        assembly {
            if lt(selfbalance(), amount) {
                mstore(0x00, 0xb12d13eb) // `ETHTransferFailed()`.
                revert(0x1c, 0x04)
            }
            if iszero(call(gasStipend, to, amount, codesize(), 0x00, codesize(), 0x00)) {
                mstore(0x00, to) // Store the address in scratch space.
                mstore8(0x0b, 0x73) // Opcode `PUSH20`.
                mstore8(0x20, 0xff) // Opcode `SELFDESTRUCT`.
                if iszero(create(amount, 0x0b, 0x16)) { revert(codesize(), codesize()) } // For gas estimation.
            }
        }
    }

    /// @dev Force sends all the ETH in the current contract to `to`, with a `gasStipend`.
    function forceSafeTransferAllETH(address to, uint256 gasStipend) internal {
        /// @solidity memory-safe-assembly
        assembly {
            if iszero(call(gasStipend, to, selfbalance(), codesize(), 0x00, codesize(), 0x00)) {
                mstore(0x00, to) // Store the address in scratch space.
                mstore8(0x0b, 0x73) // Opcode `PUSH20`.
                mstore8(0x20, 0xff) // Opcode `SELFDESTRUCT`.
                if iszero(create(selfbalance(), 0x0b, 0x16)) { revert(codesize(), codesize()) } // For gas estimation.
            }
        }
    }

    /// @dev Force sends `amount` (in wei) ETH to `to`, with `GAS_STIPEND_NO_GRIEF`.
    function forceSafeTransferETH(address to, uint256 amount) internal {
        /// @solidity memory-safe-assembly
        assembly {
            if lt(selfbalance(), amount) {
                mstore(0x00, 0xb12d13eb) // `ETHTransferFailed()`.
                revert(0x1c, 0x04)
            }
            if iszero(call(GAS_STIPEND_NO_GRIEF, to, amount, codesize(), 0x00, codesize(), 0x00)) {
                mstore(0x00, to) // Store the address in scratch space.
                mstore8(0x0b, 0x73) // Opcode `PUSH20`.
                mstore8(0x20, 0xff) // Opcode `SELFDESTRUCT`.
                if iszero(create(amount, 0x0b, 0x16)) { revert(codesize(), codesize()) } // For gas estimation.
            }
        }
    }

    /// @dev Force sends all the ETH in the current contract to `to`, with `GAS_STIPEND_NO_GRIEF`.
    function forceSafeTransferAllETH(address to) internal {
        /// @solidity memory-safe-assembly
        assembly {
            // forgefmt: disable-next-item
            if iszero(call(GAS_STIPEND_NO_GRIEF, to, selfbalance(), codesize(), 0x00, codesize(), 0x00)) {
                mstore(0x00, to) // Store the address in scratch space.
                mstore8(0x0b, 0x73) // Opcode `PUSH20`.
                mstore8(0x20, 0xff) // Opcode `SELFDESTRUCT`.
                if iszero(create(selfbalance(), 0x0b, 0x16)) { revert(codesize(), codesize()) } // For gas estimation.
            }
        }
    }

    /// @dev Sends `amount` (in wei) ETH to `to`, with a `gasStipend`.
    function trySafeTransferETH(address to, uint256 amount, uint256 gasSt

Tags:
ERC20, Multisig, Mintable, Burnable, Swap, Liquidity, Upgradeable, Multi-Signature, Factory|addr:0xb516e44f55956e9e5b15e6001d38cd0219f71851|verified:true|block:23676410|tx:0xe6db41e9b302ff749b5b85a9f6cf85a9049d6468fc2625eebbfcbecbe2826a79|first_check:1761664067

Submitted on: 2025-10-28 16:07:49

Comments

Log in to comment.

No comments yet.