Lockx

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": {
    "contracts/Lockx.sol": {
      "content": "// SPDX-License-Identifier: BUSL-1.1
// Copyright © 2025 Lockx. All rights reserved.
// This software is licensed under the Business Source License 1.1 (BUSL-1.1).
// You may use, modify, and distribute this code for non-commercial purposes only.
// For commercial use, you must obtain a license from Lockx.io.
// On or after January 1, 2029, this code will be made available under the MIT License.
pragma solidity ^0.8.30;

import '@openzeppelin/contracts/token/ERC721/ERC721.sol';
import '@openzeppelin/contracts/utils/ReentrancyGuard.sol';
import '@openzeppelin/contracts/access/Ownable.sol';
import '@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol';
import '@openzeppelin/contracts/utils/Strings.sol';
import './Withdrawals.sol';
import './SignatureVerification.sol';

/* ───────────────────────────── ERC-5192 / Soulbound standard ──────────────────────────────── */
interface IERC5192 {
    /// Emitted exactly once when a Lockbox becomes locked (non-transferable).
    event Locked(uint256 tokenId);

    /// Must always return true for every existing Lockbox.
    function locked(uint256 tokenId) external view returns (bool);
}

/**
 * @title Lockx
 * @dev Core soul-bound ERC-721 contract for Lockbox creation, key rotation, metadata management, and burning.
 *      Implements ERC-5192 (soulbound standard) for non-transferability.
 *      Inherits the Withdrawals smart contract which inherits Deposits and SignatureVerification contracts.
 */
contract Lockx is ERC721, Ownable, Withdrawals, IERC5192 {
    /// @dev Next token ID to mint (auto-incremented per mint).
    uint256 private _nextId;


    /* ─────────── Custom errors ───────── */
    error ZeroTokenAddress();
    error ArrayLengthMismatch();
    error DefaultURIAlreadySet();
    error NoURI();
    error TransfersDisabled();
    error FallbackNotAllowed();
    error DirectETHTransferNotAllowed();
    error LockboxNotEmpty();
    error DuplicateKey();


    /* ───────────────────────── Metadata storage ────────────────────────── */
    string private _defaultMetadataURI;
    mapping(uint256 => string) private _tokenMetadataURIs;

    /// Emitted whenever a per-token metadata URI is set/updated.
    event TokenMetadataURISet(uint256 indexed tokenId, bytes32 indexed referenceId);
    event Minted(uint256 indexed tokenId, bytes32 indexed referenceId);
    event LockboxBurned(uint256 indexed tokenId, bytes32 indexed referenceId);
    event KeyRotated(uint256 indexed tokenId, bytes32 indexed referenceId);


    /* ─────────────────────────── Constructor ───────────────────────────── */

    /**
     * @dev Deploys the contract and initializes the EIP-712 domain used for
     *      signature authorization in SignatureVerification.
     *      Creates the treasury lockbox (tokenId=0) and assigns it to the deployer.
     */
    constructor() ERC721('Lockx.io', 'Lockbox') Ownable(msg.sender) SignatureVerification(address(this)) {
        // Mint the treasury lockbox (tokenId = 0) to the deployer
        uint256 treasuryTokenId = _nextId++;
        _initialize(treasuryTokenId, msg.sender, bytes32(0)); // Use deployer address as initial treasury key (can be rotated later)
        _mint(msg.sender, treasuryTokenId);
        emit Locked(treasuryTokenId);
        emit Minted(treasuryTokenId, bytes32(0));
    }


    /* ───────────────────────── Minting + wrapping flows ───────────────────────── */

    /**
     * @notice Mint a new Lockbox and deposit ETH.
     * @param lockboxPublicKey The public key used for on-chain signature verification.
     * @param referenceId An external reference ID for off-chain tracking.
     *
     * Requirements:
     * - `lockboxPublicKey` must not be the zero address.
     * - `msg.value` > 0 to deposit ETH.
     */
    function createLockboxWithETH(
        address lockboxPublicKey,
        bytes32 referenceId
    ) external payable nonReentrant {
        if (msg.value == 0) revert ZeroAmount();
        
        uint256 tokenId = _createLockbox(lockboxPublicKey, referenceId);
        _depositETH(tokenId, msg.value);
    }

    /**
     * @notice Mint a new Lockbox and deposit ERC20 tokens.
     * @param lockboxPublicKey The public key used for off-chain signature verification.
     * @param tokenAddress The ERC20 token contract address to deposit.
     * @param amount The amount of ERC20 tokens to deposit.
     * @param referenceId An external reference ID for off-chain tracking.
     *
     * Requirements:
     * - `lockboxPublicKey` must not be the zero address.
     * - `tokenAddress` must not be the zero address.
     * - `amount` must be greater than zero.
     */
    function createLockboxWithERC20(
        address lockboxPublicKey,
        address tokenAddress,
        uint256 amount,
        bytes32 referenceId
    ) external nonReentrant {
        if (tokenAddress == address(0)) revert ZeroTokenAddress();
        if (amount == 0) revert ZeroAmount();
        
        uint256 tokenId = _createLockbox(lockboxPublicKey, referenceId);
        _depositERC20(tokenId, tokenAddress, amount);
    }

    /**
     * @notice Mint a new Lockbox and deposit a single ERC721.
     * @param lockboxPublicKey The public key used for off-chain signature verification.
     * @param nftContract The ERC721 contract address to deposit.
     * @param externalNftTokenId The token ID of the ERC721 to deposit.
     * @param referenceId An external reference ID for off-chain tracking.
     *
     * Requirements:
     * - `lockboxPublicKey` and `nftContract` must not be the zero address.
     */
    function createLockboxWithERC721(
        address lockboxPublicKey,
        address nftContract,
        uint256 externalNftTokenId,
        bytes32 referenceId
    ) external nonReentrant {
        if (nftContract == address(0)) revert ZeroTokenAddress();
        
        uint256 tokenId = _createLockbox(lockboxPublicKey, referenceId);
        _depositERC721(tokenId, nftContract, externalNftTokenId);
    }

    /**
     * @notice Mint a new Lockbox and perform a batch deposit of ETH, ERC20s, and ERC721s.
     * @param lockboxPublicKey The public key used for off-chain signature verification.
     * @param tokenAddresses ERC20 token contract addresses to deposit.
     * @param tokenAmounts Corresponding amounts of each ERC20 to deposit.
     * @param nftContracts ERC721 contract addresses to deposit.
     * @param nftTokenIds Corresponding token IDs of each ERC721 to deposit.
     * @param referenceId An external reference ID for off-chain tracking.
     *
     * Requirements:
     * - `lockboxPublicKey` must not be zero address.
     * - `tokenAddresses.length == tokenAmounts.length`.
     * - `nftContracts.length == nftTokenIds.length`.
     * - ETH deposits can be included via msg.value.
     */
    function createLockboxWithBatch(
        address lockboxPublicKey,
        address[] calldata tokenAddresses,
        uint256[] calldata tokenAmounts,
        address[] calldata nftContracts,
        uint256[] calldata nftTokenIds,
        bytes32 referenceId
    ) external payable nonReentrant {
        if (
            tokenAddresses.length != tokenAmounts.length ||
            nftContracts.length != nftTokenIds.length
        ) revert ArrayLengthMismatch();
        
        // Prevent empty lockbox creation - at least one asset must be provided
        if (msg.value == 0 && tokenAddresses.length == 0 && nftContracts.length == 0) {
            revert ZeroAmount();
        }
        
        uint256 tokenId = _createLockbox(lockboxPublicKey, referenceId);
        _batchDeposit(tokenId, msg.value, tokenAddresses, tokenAmounts, nftContracts, nftTokenIds);
    }


    /* ──────────────────────── Default metadata management ──────────────────────── */

    /**
     * @notice Sets the default metadata URI for all Lockboxes (only once).
     * @param newDefaultURI The base metadata URI to use for tokens without custom URIs.
     * @dev Can only be called by the contract owner, and only once.
     *
     * Requirements:
     * - Default URI must not be already set.
     */
    function setDefaultMetadataURI(string memory newDefaultURI) external onlyOwner {
        if (bytes(_defaultMetadataURI).length > 0) revert DefaultURIAlreadySet();
        _defaultMetadataURI = newDefaultURI;
    }


    /* ───────────────────────── Token-gated + EIP-712 secured metadata management ────────────────────────── */

    /**
     * @notice Sets or updates a custom metadata URI for a specific Lockbox.
     * @param tokenId The ID of the Lockbox to update.
     * @param signature The EIP-712 signature by the active Lockbox key.
     * @param newMetadataURI The new metadata URI to assign.
     * @param referenceId An external reference ID for off-chain tracking.
     * @param signatureExpiry UNIX timestamp until which the signature is valid.
     *
     * Requirements:
     * - `tokenId` must exist and caller must be its owner.
     * - `signature` must be valid and unexpired.
     */
    function setTokenMetadataURI(
        uint256 tokenId,
        bytes memory signature,
        string memory newMetadataURI,
        bytes32 referenceId,
        uint256 signatureExpiry
    ) external nonReentrant {
        // 1) Checks
        if (ownerOf(tokenId) != msg.sender) revert NotOwner();
        if (block.timestamp > signatureExpiry) revert SignatureExpired();
        _verifyReferenceId(tokenId, referenceId);

        bytes memory data = abi.encode(
            newMetadataURI,
            referenceId,
            signatureExpiry
        );
        _verifySignature(
            tokenId,
            signature,
            address(0),
            OperationType.SET_TOKEN_URI,
            data
        );

        // 2) Effects
        _tokenMetadataURIs[tokenId] = newMetadataURI;
        
        // 3) Interactions (none in this case, just emit event)
        emit TokenMetadataURISet(tokenId, referenceId);
    }

    /**
     * @notice Returns the metadata URI for a Lockbox.
     * @param tokenId The ID of the token to query.
     * @return The custom URI if set; otherwise the default URI with tokenId appended.
     * @dev Reverts if neither custom nor default URI is available.
     */
    function tokenURI(uint256 tokenId) public view override(ERC721) returns (string memory) {
        if (_ownerOf(tokenId) == address(0)) revert NonexistentToken();
        string memory custom = _tokenMetadataURIs[tokenId];
        if (bytes(custom).length > 0) return custom;
        if (bytes(_defaultMetadataURI).length > 0) {
            return string(abi.encodePacked(_defaultMetadataURI, Strings.toString(tokenId)));
        }
        revert NoURI();
    }

    
    /* ─────────────────── Lockbox key rotation ──────────────────── */

    /*
     * @notice Rotate the off-chain authorization key for a Lockbox.
     * @param tokenId         The ID of the Lockbox.
     * @param signature       The EIP-712 signature by the active Lockbox key.
     * @param newPublicKey    The new authorized Lockbox public key.
     * @param referenceId     External reference ID for off-chain tracking.
     * @param signatureExpiry UNIX timestamp after which the signature is invalid.
     *
     * Requirements:
     * - `tokenId` must exist and caller must be its owner.
     * - `block.timestamp` must be ≤ `signatureExpiry`.
     */
    function rotateLockboxKey(
        uint256 tokenId,
        bytes memory signature,
        address newPublicKey,
        bytes32 referenceId,
        uint256 signatureExpiry
    ) external nonReentrant onlyLockboxOwner(tokenId) {
        if (block.timestamp > signatureExpiry) revert SignatureExpired();

        _verifyReferenceId(tokenId, referenceId);
        
        // Check that the new key is different from the current key
        if (_getActiveLockboxPublicKey(tokenId) == newPublicKey) revert DuplicateKey();


        bytes memory data = abi.encode(
            newPublicKey,
            referenceId,
            signatureExpiry
        );
        _verifySignature(
            tokenId,
            signature,
            newPublicKey,
            OperationType.ROTATE_KEY,
            data
        );
        emit KeyRotated(tokenId, referenceId);
    }


    /* ─────────────────── Lockbox burning ──────────────────── */

    function _burnLockboxNFT(uint256 tokenId) internal {
        _burn(tokenId);
    }

    /*
     * @notice Authenticated burn of a Lockbox, clearing all assets and burning the NFT.
     * @param tokenId         The ID of the Lockbox.
     * @param signature       The EIP-712 signature by the active Lockbox key.
     * @param referenceId     External reference ID for off-chain tracking.
     * @param signatureExpiry UNIX timestamp after which the signature is invalid.
     *
     * Requirements:
     * - `tokenId` must exist and caller must be its owner.
     * - `block.timestamp` must be ≤ `signatureExpiry`.
     */
    function burnLockbox(
        uint256 tokenId,
        bytes memory signature,
        bytes32 referenceId,
        uint256 signatureExpiry
    ) external nonReentrant onlyLockboxOwner(tokenId) {
        if (block.timestamp > signatureExpiry) revert SignatureExpired();
        _verifyReferenceId(tokenId, referenceId);

        bytes memory data = abi.encode(referenceId, signatureExpiry);
        _verifySignature(
            tokenId,
            signature,
            address(0),
            OperationType.BURN_LOCKBOX,
            data
        );

        _finalizeBurn(tokenId);
        emit LockboxBurned(tokenId, referenceId);
    }
    
    /**
     * @dev Internal helper to create a new Lockbox
     * @param lockboxPublicKey The public key used for off-chain signature verification
     * @param referenceId An external reference ID for off-chain tracking
     * @return tokenId The newly minted token ID
     */
    function _createLockbox(
        address lockboxPublicKey,
        bytes32 referenceId
    ) internal returns (uint256) {
        // 1) Checks
        if (lockboxPublicKey == address(0)) revert ZeroKey();
        
        // 2) Effects
        uint256 tokenId = _nextId++;
        _initialize(tokenId, lockboxPublicKey, referenceId);
        _mint(msg.sender, tokenId);

        emit Locked(tokenId);
        emit Minted(tokenId, referenceId);
        
        return tokenId;
    }

    /**
     * @dev Internal helper called by `burnLockbox`.
     *      - Wipes all ETH / ERC20 / ERC721 bookkeeping for the Lockbox.
     *      - Delegates the actual ERC-721 burn to `_burnLockboxNFT` (implemented above).
     */
    function _finalizeBurn(uint256 tokenId) internal {
        // Check if lockbox is empty before burning
        if (_ethBalances[tokenId] > 0) revert LockboxNotEmpty();
        if (_erc20TokenAddresses[tokenId].length > 0) revert LockboxNotEmpty();
        if (_nftKeys[tokenId].length > 0) revert LockboxNotEmpty();
        
        /* ---- ETH ---- */
        delete _ethBalances[tokenId];

        /* ---- ERC-20 balances ---- */
        delete _erc20TokenAddresses[tokenId];

        /* ---- ERC-721 bookkeeping ---- */
        delete _nftKeys[tokenId];

        /* ---- finally burn the NFT itself ---- */
        _burnLockboxNFT(tokenId);
        _purgeAuth(tokenId);
    }


    /* ────────────────────── Soul-bound mechanics (ERC-5192) ────────────── */

    /**
     * @notice Always returns true for existing Lockboxes (soulbound).
     * @param tokenId The ID of the Lockbox.
     * @return Always true.
     * @dev Reverts if token does not exist.
     */
    function locked(uint256 tokenId) external view override returns (bool) {
        if (_ownerOf(tokenId) == address(0)) revert NonexistentToken();
        return true;
    }

    /// Override _update to enforce soulbound behavior (prevent transfers) and cleanup metadata on burn.
    function _update(address to, uint256 tokenId, address auth) internal override returns (address) {
        address from = _ownerOf(tokenId);
        if (from != address(0) && to != address(0)) {
            revert TransfersDisabled();
        }
        
        // Clear custom metadata on burn (when to == address(0))
        if (to == address(0)) {
            delete _tokenMetadataURIs[tokenId];
        }
        
        return super._update(to, tokenId, auth);
    }

    function supportsInterface(bytes4 interfaceId) public view override(ERC721) returns (bool) {
        // ERC-5192 soulbound interface
        if (interfaceId == type(IERC5192).interfaceId) return true;
        // ERC-721 Receiver interface
        if (interfaceId == type(IERC721Receiver).interfaceId) return true;
        // everything else (ERC-721, ERC-165)
        return super.supportsInterface(interfaceId);
    }


    /* ───────────────────────── Fallback handlers ───────────────────────── */
    
    /**
     * @notice Receive ETH only from allowed routers.
     * @dev Prevents orphaned ETH from direct transfers.
     *      Legitimate ETH comes through deposit functions and routers.
     */
    receive() external payable {
        // Only accept ETH from allowed routers
        if (!_isAllowedRouter(msg.sender)) {
            revert DirectETHTransferNotAllowed();
        }
    }
    
    fallback() external {
        revert FallbackNotAllowed();
    }
}
"
    },
    "contracts/SignatureVerification.sol": {
      "content": "// SPDX-License-Identifier: BUSL-1.1
// Copyright © 2025 Lockx. All rights reserved.
// This software is licensed under the Business Source License 1.1 (BUSL-1.1).
// You may use, modify, and distribute this code for non-commercial purposes only.
// For commercial use, you must obtain a license from Lockx.io.
// On or after January 1, 2029, this code will be made available under the MIT License.
pragma solidity ^0.8.30;

import '@openzeppelin/contracts/token/ERC721/ERC721.sol';
import '@openzeppelin/contracts/utils/cryptography/ECDSA.sol';
import '@openzeppelin/contracts/utils/cryptography/EIP712.sol';

/**
 * @title SignatureVerification
 * @notice Provides signature-based authorization for contract operations using EIP‑712.
 * @dev Each Lockbox references an active Lockbox public key that must sign operations.
 *      The contract stores a nonce to prevent replay attacks.
 */
contract SignatureVerification is EIP712 {
    using ECDSA for bytes32;

    /// @notice Enumerates the possible operations that require Lockbox key authorization.
    enum OperationType {
        ROTATE_KEY,
        WITHDRAW_ETH,
        WITHDRAW_ERC20,
        WITHDRAW_NFT,
        BURN_LOCKBOX,
        SET_TOKEN_URI,
        BATCH_WITHDRAW,
        SWAP_ASSETS
    }

    /// @dev Gas-cheap pointer to the Lockbox ERC-721 (set once in constructor).
    ERC721 immutable _erc721;

    /**
     * @dev Stores authorization data for each Lockbox.
     * @param nonce Monotonically increasing value to prevent signature replay.
     * @param activeLockboxPublicKey The public key currently authorized to sign operations for this Lockbox.
     */
    struct TokenAuth {
        address activeLockboxPublicKey;
        uint96 nonce;
        bytes32 referenceId;
    }

    /// @dev Mapping from Lockbox token ID to its TokenAuth.
    mapping(uint256 => TokenAuth) private _tokenAuth;


    /* ─────────────────── Errors ────────────────────── */
    error NotOwner();
    error InvalidSignature();
    error AlreadyInitialized();
    error ZeroKey();
    error InvalidReferenceId();


    /* ─────────────────── EIP-712 setup ───────────────────── */

    /**
     * @dev Typehash for the operation, including tokenId, nonce, opType (as uint8),
     *      and a bytes32 hash of the data.
     */
    bytes32 private constant OPERATION_TYPEHASH =
        keccak256('Operation(uint256 tokenId,uint256 nonce,uint8 opType,bytes32 dataHash)');

    /**
     * @notice Constructor that sets the reference to the ERC721 contract for Lockbox ownership checks.
     * @param erc721Address The address of the ERC721 contract that mints/owns the Lockboxs.
     */
    constructor(address erc721Address) EIP712('Lockx', '5') {
        _erc721 = ERC721(erc721Address);
    }

    /**
     * @notice Initializes the Lockbox data with a public key, nonce, and referenceId.
     * @dev Intended to be called once upon minting a new Lockbox.
     * @param tokenId The ID of the Lockbox being initialized.
     * @param lockboxPublicKey The public key that will sign operations for this Lockbox.
     * @param referenceId The off-chain tracking identifier for this Lockbox.
     */
    function _initialize(uint256 tokenId, address lockboxPublicKey, bytes32 referenceId) internal {
        if (_tokenAuth[tokenId].activeLockboxPublicKey != address(0)) {
            revert AlreadyInitialized();
        }

        _tokenAuth[tokenId].activeLockboxPublicKey = lockboxPublicKey;
        _tokenAuth[tokenId].nonce = 0;
        _tokenAuth[tokenId].referenceId = referenceId;
    }

    /**
     * @notice Modifier that checks the caller is the owner of the specified token.
     * @param tokenId The ID of the Lockbox to check ownership against.
     */
    modifier onlyTokenOwner(uint256 tokenId) {
        if (_erc721.ownerOf(tokenId) != msg.sender) revert NotOwner();
        _;
    }

    /**
     * @notice Verifies an EIP‑712 signature for a specific operation.
     * @param tokenId The ID of the Lockbox.
     * @param signature The Lockbox private key signature to verify.
     * @param newLockboxPublicKey The new Lockbox public key (if rotating the key).
     * @param opType The operation being authorized.
     * @param data Encoded parameters for the specific operation.
     *
     * Requirements:
     * - The signature must be valid for the current, active Lockbox public key.
     * - On successful verification, the nonce increments.
     * - If `opType` is `ROTATE_KEY`, the Lockbox public key is updated to `newLockboxPublicKey`.
     */
    function _verifySignature(
        uint256 tokenId,
        bytes memory signature,
        address newLockboxPublicKey,
        OperationType opType,
        bytes memory data
    ) internal {
        TokenAuth storage tokenAuth = _tokenAuth[tokenId];

        bytes32 structHash = keccak256(
            abi.encode(OPERATION_TYPEHASH, tokenId, tokenAuth.nonce, uint8(opType), keccak256(data))
        );
        bytes32 expectedHash = _hashTypedDataV4(structHash);

        address signer = expectedHash.recover(signature);
        if (signer != tokenAuth.activeLockboxPublicKey) {
            revert InvalidSignature();
        }

        // Increment nonce after successful verification.
        tokenAuth.nonce++;

        // If rotating the key, update the active Lockbox public key.
        if (opType == OperationType.ROTATE_KEY && newLockboxPublicKey != address(0)) {
            tokenAuth.activeLockboxPublicKey = newLockboxPublicKey;
        }
    }

    function _purgeAuth(uint256 tokenId) internal {
        delete _tokenAuth[tokenId];
    }


    /* ─────────────────── Token-gated view functions ────────────────────── */

    /**
     * @notice Retrieves the current Lockbox public key for the given Lockbox.
     * @param tokenId The ID of the Lockbox.
     * @return The currently active Lockbox public key.
     *
     * Requirements:
     * - Caller must be the owner of `tokenId`.
     */
    function getActiveLockboxPublicKeyForToken(
        uint256 tokenId
    ) external view onlyTokenOwner(tokenId) returns (address) {
        return _tokenAuth[tokenId].activeLockboxPublicKey;
    }

    /**
     * @notice Retrieves the current nonce for the given Lockbox.
     * @param tokenId The ID of the Lockbox.
     * @return The current nonce used for signature verification.
     *
     * Requirements:
     * - Caller must be the owner of `tokenId`.
     */
    function getNonce(uint256 tokenId) external view onlyTokenOwner(tokenId) returns (uint256) {
        return uint256(_tokenAuth[tokenId].nonce);
    }
    
    /**
     * @dev Internal function to verify that the provided referenceId matches the stored one.
     * @param tokenId The ID of the Lockbox.
     * @param referenceId The referenceId to verify.
     */
    function _verifyReferenceId(uint256 tokenId, bytes32 referenceId) internal view {
        if (_tokenAuth[tokenId].referenceId != referenceId) {
            revert InvalidReferenceId();
        }
    }
    
    /**
     * @dev Internal function to get the stored referenceId for a Lockbox.
     * @param tokenId The ID of the Lockbox.
     * @return The stored referenceId.
     */
    function _getReferenceId(uint256 tokenId) internal view returns (bytes32) {
        return _tokenAuth[tokenId].referenceId;
    }
    
    /**
     * @dev Internal function to get the current active key for a Lockbox.
     * @param tokenId The ID of the Lockbox.
     * @return The currently active Lockbox public key.
     */
    function _getActiveLockboxPublicKey(uint256 tokenId) internal view returns (address) {
        return _tokenAuth[tokenId].activeLockboxPublicKey;
    }
}
"
    },
    "contracts/Withdrawals.sol": {
      "content": "// SPDX-License-Identifier: BUSL-1.1
// Copyright © 2025 Lockx. All rights reserved.
// This software is licensed under the Business Source License 1.1 (BUSL-1.1).
// You may use, modify, and distribute this code for non-commercial purposes only.
// For commercial use, you must obtain a license from Lockx.io.
// On or after January 1, 2029, this code will be made available under the MIT License.
pragma solidity ^0.8.30;

import '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol';
import '@openzeppelin/contracts/token/ERC20/IERC20.sol';
import '@openzeppelin/contracts/utils/ReentrancyGuard.sol';
import '@openzeppelin/contracts/token/ERC721/IERC721.sol';
import './Deposits.sol';

/**
 * @title Withdrawals
 * @dev Signature-gated withdraw, swap, and view helpers.
 *      Inherits Deposits for storage & deposit helpers, and
 *      SignatureVerification for EIP-712 auth.
 */
abstract contract Withdrawals is Deposits {
    using SafeERC20 for IERC20;


    /* ───────── Enums ───────── */
    enum SwapMode {
        EXACT_IN,   // Specify exact input amount, get variable output
        EXACT_OUT   // Specify exact output amount, use variable input
    }

    /* ───────── Treasury Constants ───────── */
    uint256 public constant TREASURY_LOCKBOX_ID = 0;
    uint256 public constant SWAP_FEE_BP = 10;
    uint256 private constant FEE_DIVISOR = 10000;


    /* ───────── Events ───────── */
    event Withdrawn(uint256 indexed tokenId, bytes32 indexed referenceId);
    event SwapExecuted(uint256 indexed tokenId, bytes32 indexed referenceId);
    

    /* ───────── Errors ───────── */
    error NoETHBalance();
    error InsufficientTokenBalance();
    error NFTNotFound();
    error EthTransferFailed();
    error SignatureExpired();
    error SwapCallFailed();
    error InvalidSwap();
    error SlippageExceeded();
    error RouterOverspent();
    error InsufficientOutput();
    error DuplicateEntry();
    error UnsortedArray();
    error InvalidRecipient();
    error UnauthorizedRouter();
    error UnauthorizedSelector();


    /* ─────────────────── Lockbox withdrawals ───────────────────── */

    /*
     * @notice Withdraw ETH from a Lockbox, authorized via EIP-712 signature.
     * @param tokenId         The ID of the Lockbox.
     * @param signature       The EIP-712 signature by the active Lockbox key.
     * @param amountETH       The amount of ETH to withdraw.
     * @param recipient       The address receiving the ETH.
     * @param referenceId     External reference ID for off-chain tracking.
     * @param signatureExpiry UNIX timestamp after which the signature is invalid.
     *
     * Requirements:
     * - `tokenId` must exist and caller must be its owner.
     * - `recipient` must not be the zero address.
     * - `block.timestamp` must be ≤ `signatureExpiry`.
     * - Lockbox must have ≥ `amountETH` ETH
     */
    function withdrawETH(
        uint256 tokenId,
        bytes memory signature,
        uint256 amountETH,
        address recipient,
        bytes32 referenceId,
        uint256 signatureExpiry
    ) external nonReentrant onlyLockboxOwner(tokenId) {
        // 1) Checks
        if (recipient == address(0)) revert ZeroAddress();
        if (recipient == address(this)) revert InvalidRecipient();
        if (block.timestamp > signatureExpiry) revert SignatureExpired();
        
        // Check balance before signature verification
        uint256 currentBal = _ethBalances[tokenId];
        if (currentBal < amountETH) revert NoETHBalance();
        _verifyReferenceId(tokenId, referenceId);

        // 2) Verify
        bytes memory data = abi.encode(
            amountETH,
            recipient,
            referenceId,
            signatureExpiry
        );
        _verifySignature(
            tokenId,
            signature,
            address(0),
            OperationType.WITHDRAW_ETH,
            data
        );

        // 3) Effects + Interaction
        _withdrawETH(tokenId, amountETH, recipient);

        emit Withdrawn(tokenId, referenceId);
    }

    /*
     * @notice Withdraw an ERC-20 token from a Lockbox, authorized via EIP-712 signature.
     * @param tokenId         The ID of the Lockbox.
     * @param signature       The EIP-712 signature by the active Lockbox key.
     * @param tokenAddress    The ERC-20 token address to withdraw.
     * @param amount          The amount of tokens to withdraw.
     * @param recipient       The address receiving the tokens.
     * @param referenceId     External reference ID for off-chain tracking.
     * @param signatureExpiry UNIX timestamp after which the signature is invalid.
     *
     * Requirements:
     * - `tokenId` must exist and caller must be its owner.
     * - `recipient` must not be the zero address.
     * - `block.timestamp` must be ≤ `signatureExpiry`.
     * - Lockbox must have ≥ `amount` balance of `tokenAddress`.
     */
    function withdrawERC20(
        uint256 tokenId,
        bytes memory signature,
        address tokenAddress,
        uint256 amount,
        address recipient,
        bytes32 referenceId,
        uint256 signatureExpiry
    ) external nonReentrant onlyLockboxOwner(tokenId) {
        // 1) Checks
        if (recipient == address(0)) revert ZeroAddress();
        if (recipient == address(this)) revert InvalidRecipient();
        if (block.timestamp > signatureExpiry) revert SignatureExpired();
        
        // Check balance before signature verification
        mapping(address => uint256) storage balMap = _erc20Balances[tokenId];
        uint256 bal = balMap[tokenAddress];
        if (bal < amount) revert InsufficientTokenBalance();

        _verifyReferenceId(tokenId, referenceId);

        // 2) Verify
        bytes memory data = abi.encode(
            tokenAddress,
            amount,
            recipient,
            referenceId,
            signatureExpiry
        );
        _verifySignature(
            tokenId,
            signature,
            address(0),
            OperationType.WITHDRAW_ERC20,
            data
        );

        // 3) Effects + Interaction
        _withdrawERC20(tokenId, tokenAddress, amount, recipient);

        emit Withdrawn(tokenId, referenceId);
    }

    /*
     * @notice Withdraw an ERC-721 token from a Lockbox, authorized via EIP-712 signature.
     * @param tokenId         The ID of the Lockbox.
     * @param signature       The EIP-712 signature by the active Lockbox key.
     * @param nftContract     The ERC-721 contract address to withdraw.
     * @param nftTokenId      The token ID of the ERC-721 to withdraw.
     * @param recipient       The address receiving the NFT.
     * @param referenceId     External reference ID for off-chain tracking.
     * @param signatureExpiry UNIX timestamp after which the signature is invalid.
     *
     * Requirements:
     * - `tokenId` must exist and caller must be its owner.
     * - `recipient` must not be the zero address.
     * - `block.timestamp` must be ≤ `signatureExpiry`.
     * - The specified NFT must be deposited in this Lockbox.
     */
    function withdrawERC721(
        uint256 tokenId,
        bytes memory signature,
        address nftContract,
        uint256 nftTokenId,
        address recipient,
        bytes32 referenceId,
        uint256 signatureExpiry
    ) external nonReentrant onlyLockboxOwner(tokenId) {
        // 1) Checks
        if (recipient == address(0)) revert ZeroAddress();
        if (recipient == address(this)) revert InvalidRecipient();
        if (block.timestamp > signatureExpiry) revert SignatureExpired();
        
        // Check NFT ownership before signature verification
        bytes32 key = keccak256(abi.encodePacked(nftContract, nftTokenId));
        if (_lockboxNftData[tokenId][key].nftContract == address(0)) revert NFTNotFound();
        _verifyReferenceId(tokenId, referenceId);

        // 2) Verify
        bytes memory data = abi.encode(
            nftContract,
            nftTokenId,
            recipient,
            referenceId,
            signatureExpiry
        );
        _verifySignature(
            tokenId,
            signature,
            address(0),
            OperationType.WITHDRAW_NFT,
            data
        );

        // 3) Effects + Interaction
        _withdrawERC721(tokenId, nftContract, nftTokenId, recipient);

        emit Withdrawn(tokenId, referenceId);
    }

    /*
     * @notice Batch withdrawal of ETH, ERC-20s, and ERC-721s with a single signature.
     * @param tokenId         The ID of the Lockbox.
     * @param signature       The EIP-712 signature by the active Lockbox key.
     * @param amountETH       The amount of ETH to withdraw.
     * @param tokenAddresses  The list of ERC-20 token addresses to withdraw.
     * @param tokenAmounts    The corresponding amounts of each ERC-20 to withdraw.
     * @param nftContracts    The list of ERC-721 contract addresses to withdraw.
     * @param nftTokenIds     The corresponding ERC-721 token IDs to withdraw.
     * @param recipient       The address receiving all assets.
     * @param referenceId     External reference ID for off-chain tracking.
     * @param signatureExpiry UNIX timestamp after which the signature is invalid.
     *
     * Requirements:
     * - `tokenId` must exist and caller must be its owner.
     * - `recipient` must not be the zero address.
     * - `block.timestamp` must be ≤ `signatureExpiry`.
     * - `tokenAddresses.length` must equal `tokenAmounts.length`.
     * - `nftContracts.length` must equal `nftTokenIds.length`.
     * - `tokenAddresses` must be sorted in strictly ascending order (no duplicates).
     * - NFT pairs `(nftContract, nftTokenId)` must be sorted in strictly ascending lexicographic order
     *   by `(nftContract, nftTokenId)` (no duplicates).
     * - Lockbox must have ≥ `amountETH` ETH and sufficient balances for each asset.
     */
    function batchWithdraw(
        uint256 tokenId,
        bytes memory signature,
        uint256 amountETH,
        address[] calldata tokenAddresses,
        uint256[] calldata tokenAmounts,
        address[] calldata nftContracts,
        uint256[] calldata nftTokenIds,
        address recipient,
        bytes32 referenceId,
        uint256 signatureExpiry
    ) external nonReentrant onlyLockboxOwner(tokenId) {
        // 1) Checks
        if (recipient == address(0)) revert ZeroAddress();
        if (recipient == address(this)) revert InvalidRecipient();
        if (block.timestamp > signatureExpiry) revert SignatureExpired();
        if (
            tokenAddresses.length != tokenAmounts.length ||
            nftContracts.length != nftTokenIds.length
        ) revert MismatchedInputs();
        
        // Check ETH balance
        if (amountETH > 0) {
            uint256 currentBal = _ethBalances[tokenId];
            if (currentBal < amountETH) revert NoETHBalance();
        }
        
        // Check ERC-20 balances
        mapping(address => uint256) storage balMap = _erc20Balances[tokenId];
        for (uint256 i; i < tokenAddresses.length; ) {
            if (balMap[tokenAddresses[i]] < tokenAmounts[i]) revert InsufficientTokenBalance();
            unchecked { ++i; }
        }
        
        // Check NFT ownership
        for (uint256 i; i < nftContracts.length; ) {
            bytes32 key = keccak256(abi.encodePacked(nftContracts[i], nftTokenIds[i]));
            if (_lockboxNftData[tokenId][key].nftContract == address(0)) revert NFTNotFound();
            unchecked { ++i; }
        }
        _verifyReferenceId(tokenId, referenceId);

        // 2) Verify
        bytes memory data = abi.encode(
            amountETH,
            tokenAddresses,
            tokenAmounts,
            nftContracts,
            nftTokenIds,
            recipient,
            referenceId,
            signatureExpiry
        );
        _verifySignature(
            tokenId,
            signature,
            address(0),
            OperationType.BATCH_WITHDRAW,
            data
        );

        // 3) Effects + Interactions for each asset type
        if (amountETH > 0) {
            _withdrawETH(tokenId, amountETH, recipient);
        }

        // — ERC-20s — enforce strictly increasing addresses (no duplicates)
        mapping(address => uint256) storage lockboxTokenBalances = _erc20Balances[tokenId];
        address previousTokenAddress;
        bool hasPreviousTokenAddress;
        for (uint256 i; i < tokenAddresses.length; ) {
            address tokenAddress = tokenAddresses[i];
            uint256 tokenAmount = tokenAmounts[i];

            if (hasPreviousTokenAddress) {
                if (uint256(uint160(tokenAddress)) <= uint256(uint160(previousTokenAddress))) revert UnsortedArray();
            } else {
                hasPreviousTokenAddress = true;
            }

            uint256 currentBalance = lockboxTokenBalances[tokenAddress];
            if (currentBalance < tokenAmount) revert InsufficientTokenBalance();
            unchecked {
                lockboxTokenBalances[tokenAddress] = currentBalance - tokenAmount;
            }

            if (lockboxTokenBalances[tokenAddress] == 0) {
                delete lockboxTokenBalances[tokenAddress];
                _removeERC20Token(tokenId, tokenAddress);
            }

            IERC20(tokenAddress).safeTransfer(recipient, tokenAmount);
            previousTokenAddress = tokenAddress;
            unchecked { ++i; }
        }

        // — ERC-721s — enforce strictly increasing lexicographic order by (contract, tokenId)
        address previousNftContract;
        uint256 previousNftTokenId;
        bool hasPreviousNft;
        for (uint256 i; i < nftContracts.length; ) {
            address nftContract = nftContracts[i];
            uint256 nftTokenId = nftTokenIds[i];

            if (hasPreviousNft) {
                if (nftContract < previousNftContract || (nftContract == previousNftContract && nftTokenId <= previousNftTokenId)) revert UnsortedArray();
            } else {
                hasPreviousNft = true;
            }

            bytes32 key = keccak256(abi.encodePacked(nftContract, nftTokenId));
            if (_lockboxNftData[tokenId][key].nftContract == address(0)) revert NFTNotFound();

            delete _lockboxNftData[tokenId][key];
            _removeNFTKey(tokenId, key);

            IERC721(nftContract).safeTransferFrom(address(this), recipient, nftTokenId);
            previousNftContract = nftContract;
            previousNftTokenId = nftTokenId;
            unchecked { ++i; }
        }

        emit Withdrawn(tokenId, referenceId);
    }

    /*
     * @notice Execute an asset swap within a Lockbox, authorized via EIP-712 signature.
     * @param tokenId         The ID of the Lockbox.
     * @param signature       The EIP-712 signature by the active Lockbox key.
     * @param tokenIn         The input token address (address(0) for ETH).
     * @param tokenOut        The output token address (address(0) for ETH).
     * @param swapMode        Whether this is EXACT_IN or EXACT_OUT swap.
     * @param amountSpecified For EXACT_IN: input amount. For EXACT_OUT: desired output amount.
     * @param amountLimit     For EXACT_IN: min output. For EXACT_OUT: max input allowed.
     * @param target          The router/aggregator contract address to execute swap.
     * @param data            The pre-built calldata for the swap execution.
     * @param referenceId     External reference ID for off-chain tracking.
     * @param signatureExpiry UNIX timestamp after which the signature is invalid.
     * @param recipient       The recipient address for swap output. Use address(0) to credit lockbox.
     *
     * Requirements:
     * - `tokenId` must exist and caller must be its owner.
     * - `block.timestamp` must be < `signatureExpiry`.
     * - For EXACT_IN: Lockbox must have ≥ amountSpecified of tokenIn.
     * - For EXACT_OUT: Lockbox must have ≥ amountLimit of tokenIn (max you're willing to spend).
     * - `target` must be an allowed router.
     * - For EXACT_IN: Must receive ≥ amountLimit of tokenOut (min acceptable output).
     * - For EXACT_OUT: Must receive ≥ amountSpecified of tokenOut and spend ≤ amountLimit of tokenIn.
     * - If `recipient` is address(0), output is credited to lockbox, otherwise sent to recipient.
     */
    function swapInLockbox(
        uint256 tokenId,
        bytes memory signature,
        address tokenIn,
        address tokenOut,
        SwapMode swapMode,
        uint256 amountSpecified,
        uint256 amountLimit,
        address target,
        bytes calldata data,
        bytes32 referenceId,
        uint256 signatureExpiry,
        address recipient
    ) external nonReentrant onlyLockboxOwner(tokenId) {
        if (block.timestamp > signatureExpiry) revert SignatureExpired();
        if (amountSpecified == 0) revert ZeroAmount();
        if (tokenIn == tokenOut) revert InvalidSwap();

        // Validate router and calldata selector (also handles zero address)
        if (!_isAllowedRouter(target)) revert UnauthorizedRouter();
        if (!_isAllowedSelector(data)) revert UnauthorizedSelector();
        _verifyReferenceId(tokenId, referenceId);

        // 1) For EXACT_IN/EXACT_OUT, check balance sufficiency upfront (deterministic pre-check)
        if (swapMode == SwapMode.EXACT_IN) {
            if (tokenIn == address(0)) {
                if (_ethBalances[tokenId] < amountSpecified) revert NoETHBalance();
            } else {
                if (_erc20Balances[tokenId][tokenIn] < amountSpecified) revert InsufficientTokenBalance();
            }
        } else {
            // For EXACT_OUT, check maximum input allowed
            if (tokenIn == address(0)) {
                if (_ethBalances[tokenId] < amountLimit) revert NoETHBalance();
            } else {
                if (_erc20Balances[tokenId][tokenIn] < amountLimit) revert InsufficientTokenBalance();
            }
        }

        // 2) Verify signature
        bytes memory authData = abi.encode(
            tokenIn,
            tokenOut,
            uint8(swapMode),
            amountSpecified,
            amountLimit,
            target,
            keccak256(data),
            referenceId,
            signatureExpiry,
            recipient
        );
        _verifySignature(
            tokenId,
            signature,
            address(0),
            OperationType.SWAP_ASSETS,
            authData
        );

        // 3) Measure balances before swap
        uint256 balanceInBefore;
        if (tokenIn == address(0)) {
            balanceInBefore = address(this).balance;
        } else {
            balanceInBefore = IERC20(tokenIn).balanceOf(address(this));
        }
        
        uint256 balanceOutBefore;
        if (tokenOut == address(0)) {
            balanceOutBefore = address(this).balance;
        } else {
            balanceOutBefore = IERC20(tokenOut).balanceOf(address(this));
        }

        // 4) Execute swap with approval
        uint256 approvalAmount;
        uint256 ethValue;
        
        if (swapMode == SwapMode.EXACT_IN) {
            approvalAmount = amountSpecified;
            ethValue = (tokenIn == address(0)) ? amountSpecified : 0;
        } else {
            // For EXACT_OUT, approve the maximum we're willing to spend
            approvalAmount = amountLimit;
            ethValue = (tokenIn == address(0)) ? amountLimit : 0;
        }
        
        if (tokenIn != address(0)) {
            IERC20(tokenIn).forceApprove(target, approvalAmount);
        }
        
        (bool success,) = target.call{value: ethValue}(data);
        
        // Clean up approval  
        if (tokenIn != address(0)) {
            IERC20(tokenIn).approve(target, 0);
        }
        
        if (!success) revert SwapCallFailed();

        // 5) Measure actual amounts transferred
        uint256 balanceInAfter;
        if (tokenIn == address(0)) {
            balanceInAfter = address(this).balance;
        } else {
            balanceInAfter = IERC20(tokenIn).balanceOf(address(this));
        }
        
        uint256 balanceOutAfter;
        if (tokenOut == address(0)) {
            balanceOutAfter = address(this).balance;
        } else {
            balanceOutAfter = IERC20(tokenOut).balanceOf(address(this));
        }
        
        // Calculate actual amounts (handles fee-on-transfer tokens)
        uint256 actualAmountIn = balanceInBefore - balanceInAfter;
        uint256 actualAmountOut = balanceOutAfter - balanceOutBefore;

        // 6) Calculate fee first and derive userAmount (net-of-fee)
        uint256 feeAmount = (actualAmountOut * SWAP_FEE_BP + FEE_DIVISOR - 1) / FEE_DIVISOR;
        uint256 userAmount = actualAmountOut - feeAmount;

        // 7) Validate swap based on mode using net-of-fee output for slippage
        if (swapMode == SwapMode.EXACT_IN) {
            // For EXACT_IN: verify user receives at least the minimum output after fees
            if (userAmount < amountLimit) revert SlippageExceeded();
            // Router shouldn't take more than specified
            if (actualAmountIn > amountSpecified) revert RouterOverspent();
        } else {
            // For EXACT_OUT: verify we didn't spend more than maximum
            if (actualAmountIn > amountLimit) revert SlippageExceeded();
            // User should get at least the specified output after fees
            if (userAmount < amountSpecified) revert InsufficientOutput();
        }

        // 8) Update accounting with actual amounts (handles fee-on-transfer)
        // Deduct actual input amount
        if (tokenIn == address(0)) {
            _ethBalances[tokenId] -= actualAmountIn;
        } else {
            _erc20Balances[tokenId][tokenIn] -= actualAmountIn;
            
            // Clean up if balance is now 0
            if (_erc20Balances[tokenId][tokenIn] == 0) {
                delete _erc20Balances[tokenId][tokenIn];
                _removeERC20Token(tokenId, tokenIn);
            }
        }
        
        // Credit fee to treasury lockbox
        if (feeAmount > 0) {
            _creditToLockbox(TREASURY_LOCKBOX_ID, tokenOut, feeAmount);
        }
        
        // Credit user amount to recipient or lockbox
        if (recipient != address(0)) {
            // Send directly to external recipient
            if (tokenOut == address(0)) {
                (bool ethSuccess, ) = payable(recipient).call{value: userAmount}('');
                if (!ethSuccess) revert EthTransferFailed();
            } else {
                IERC20(tokenOut).safeTransfer(recipient, userAmount);
            }
        } else {
            // Credit to user's lockbox
            _creditToLockbox(tokenId, tokenOut, userAmount);
        }

        emit SwapExecuted(tokenId, referenceId);
    }


    /* ─────────────────── View helpers ──────────────────── */

    /*
     * @notice Returns the full contents of a Lockbox: ETH, ERC-20 balances, and ERC-721s.
     * @param tokenId The ID of the Lockbox.
     * @return ethBalances      The ETH amount held.
     * @return erc20Tokens Array of (tokenAddress, balance) for each ERC-20.
     * @return nfts        Array of nftBalances structs representing each ERC-721.
     *
     * Requirements:
     * - `tokenId` must exist and caller must be its owner.
     */
    struct erc20Balances {
        address tokenAddress;
        uint256 balance;
    }

    function getFullLockbox(
        uint256 tokenId
    )
        external
        view
        returns (
            uint256 lockboxETH,
            erc20Balances[] memory erc20Tokens,
            nftBalances[] memory nftContracts
        )
    {
        if (_erc721.ownerOf(tokenId) != msg.sender) revert NotOwner();

        lockboxETH = _ethBalances[tokenId];

        // ERC-20s
        address[] storage tokenAddresses = _erc20TokenAddresses[tokenId];
        erc20Tokens = new erc20Balances[](tokenAddresses.length);
        for (uint256 i; i < tokenAddresses.length; ) {
            erc20Tokens[i] = erc20Balances({
                tokenAddress: tokenAddresses[i],
                balance: _erc20Balances[tokenId][tokenAddresses[i]]
            });
            unchecked {
                ++i;
            }
        }

        // ERC-721s
        bytes32[] storage nftList = _nftKeys[tokenId];
        uint256 count;
        for (uint256 i; i < nftList.length; ) {
            if (_lockboxNftData[tokenId][nftList[i]].nftContract != address(0)) count++;
            unchecked {
                ++i;
            }
        }
        nftContracts = new nftBalances[](count);
        uint256 index;
        for (uint256 i; i < nftList.length; ) {
            if (_lockboxNftData[tokenId][nftList[i]].nftContract != address(0)) {
                nftContracts[index++] = _lockboxNftData[tokenId][nftList[i]];
            }
            unchecked {
                ++i;
            }
        }
    }


    /* ─────────────────── Internal helpers ────────────────────── */
    
    /**
     * @dev Internal helper to credit tokens to a lockbox
     * @param tokenId The lockbox token ID
     * @param token The token address (address(0) for ETH)
     * @param amount Amount to credit
     */
    function _creditToLockbox(
        uint256 tokenId, 
        address token, 
        uint256 amount
    ) internal {
        if (token == address(0)) {
            _ethBalances[tokenId] += amount;
        } else {
            // Register token if new for lockbox
            if (_erc20Balances[tokenId][token] == 0) {
                _erc20TokenAddresses[tokenId].push(token);
                _erc20Index[tokenId][token] = _erc20TokenAddresses[tokenId].length - 1;
            }
            _erc20Balances[tokenId][token] += amount;
        }
    }
    
    /**
     * @dev Internal helper to withdraw ETH from a lockbox
     * @param tokenId The lockbox token ID
     * @param amountETH Amount of ETH to withdraw
     * @param recipient Address to receive the ETH
     */
    function _withdrawETH(
        uint256 tokenId,
        uint256 amountETH,
        address recipient
    ) internal {
        _ethBalances[tokenId] -= amountETH;
        
        (bool success, ) = payable(recipient).call{value: amountETH}('');
        if (!success) revert EthTransferFailed();
    }
    
    /**
     * @dev Internal helper to withdraw ERC20 tokens from a lockbox
     * @param tokenId The lockbox token ID
     * @param tokenAddress The ERC20 token address
     * @param amount Amount of tokens to withdraw
     * @param recipient Address to receive the tokens
     */
    function _withdrawERC20(
        uint256 tokenId,
        address tokenAddress,
        uint256 amount,
        address recipient
    ) internal {
        mapping(address => uint256) storage balMap = _erc20Balances[tokenId];
        
        unchecked {
            balMap[tokenAddress] -= amount;
        }
        
        if (balMap[tokenAddress] == 0) {
            delete balMap[tokenAddress];
            _removeERC20Token(tokenId, tokenAddress);
        }
        
        IERC20(tokenAddress).safeTransfer(recipient, amount);
    }
    
    /**
     * @dev Internal helper to withdraw an ERC721 NFT from a lockbox
     * @param tokenId The lockbox token ID
     * @param nftContract The ERC721 contract address
     * @param nftTokenId The NFT token ID
     * @param recipient Address to receive the NFT
     */
    function _withdrawERC721(
        uint256 tokenId,
        address nftContract,
        uint256 nftTokenId,
        address recipient
    ) internal {
        bytes32 key = keccak256(abi.encodePacked(nftContract, nftTokenId));
        
        delete _lockboxNftData[tokenId][key];
        _removeNFTKey(tokenId, key);
        
        IERC721(nftContract).safeTransferFrom(address(this), recipient, nftTokenId);
    }
    
    /**
     * @dev Check if a router is in the immutable allowlist.
     * @param router The router address to check.
     * @return bool True if the router is allowed.
     */
    function _isAllowedRouter(address router) internal pure returns (bool) {
        return
            // Uniswap V3 SwapRouter02
            router == 0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45 ||
            // Uniswap v4 Router
            router == 0x66a9893cC07D91D95644AEDD05D03f95e1dBA8Af ||
            // 1inch Aggregation Router v6
            router == 0x111111125421cA6dc452d289314280a0f8842A65 ||
            // 0x Exchange Proxy
            router == 0xDef1C0ded9bec7F1a1670819833240f027b25EfF ||
            // Paraswap Augustus
            router == 0xDEF171Fe48CF0115B1d80b88dc8eAB59176FEe57 ||
            // CowSwap GPv2 Settlement
            router == 0x9008D19f58AAbD9eD0D60971565AA8510560ab41;
    }

    /**
     * @dev Check if the calldata selector is allowed for swap operations.
     * Prevents arbitrary function calls by whitelisting safe swap selectors.
     * @param data The calldata to validate
     * @return bool True if the selector is allowed for swaps
     */
    function _isAllowedSelector(bytes calldata data) private pure returns (bool) {
        if (data.length < 4) return false;
        
        bytes4 selector = bytes4(data[:4]);
        
        return
            // Uniswap V3 Router02
            selector == 0x04e45aaf || // exactInputSingle((address,address,uint24,address,uint256,uint256,uint160))
            selector == 0x5023b4df || // exactOutputSingle((address,address,uint24,address,uint256,uint256,uint160))
            selector == 0xc04b8d59 || // exactInput((bytes,address,uint256,uint256))
            selector == 0xf28c0498 || // exactOutput((bytes,address,uint256,uint256))
            selector == 0xac9650d8 || // multicall(bytes[])
            selector == 0x5ae401dc || // multicall(uint256,bytes[])
            selector == 0x49404b7c || // unwrapWETH9(uint256,address)
            selector == 0x12210e8a || // refundETH()
            
            // Uniswap v4 Router
            selector == 0x3593564c || // execute(bytes,bytes[],uint256)
            selector == 0x24856bc3 || // execute(bytes,bytes[])
            
            // 1inch v6
            selector == 0x07ed2379 || // swap(address,(...),bytes)
            
            // 0x Protocol (Exchange Proxy)
            selector == 0x415565b0 || // transformERC20
            selector == 0xd9627aa4 || // sellToUniswap
            
            // Paraswap Augustus
            selector == 0x54e3f31b || // simpleSwap
            selector == 0xa94e78ef || // multiSwap
            
            // CowSwap GPv2 Settlement
            selector == 0x13d79a0b;   // settle
    }


    /**
     * @notice Get list of all allowed routers (for transparency).
     * @return address[] Array of allowed router addresses.
     */
    function getAllowedRouters() external pure returns (address[] memory) {
        address[] memory routers = new address[](6);
        routers[0] = 0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45; // Uniswap V3 SwapRouter02
        routers[1] = 0x66a9893cC07D91D95644AEDD05D03f95e1dBA8Af; // Uniswap v4 Router
        routers[2] = 0x111111125421cA6dc452d289314280a0f8842A65; // 1inch v6
        routers[3] = 0xDef1C0ded9bec7F1a1670819833240f027b25EfF; // 0x
        routers[4] = 0xDEF171Fe48CF0115B1d80b88dc8eAB59176FEe57; // Paraswap
        routers[5] = 0x9008D19f58AAbD9eD0D60971565AA8510560ab41; // Cowswap
        return routers;
    }

    /**
     * @notice Check if a router is allowed (public helper).
     * @param router The router address to check.
     * @return bool True if the router is allowed.
     */
    function isAllowedRouter(address router) external pure returns (bool) {
        return _isAllowedRouter(router);
    }
}
"
    },
    "@openzeppelin/contracts/utils/Strings.sol": {
      "content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.4.0) (utils/Strings.sol)

pragma solidity ^0.8.20;

import {Math} from "./math/Math.sol";
import {SafeCast} from "./math/SafeCast.sol";
import {SignedMath} from "./math/SignedMath.sol";

/**
 * @dev String operations.
 */
library Strings {
    using SafeCast for *;

    bytes16 private constant HEX_DIGITS = "0123456789abcdef";
    uint8 private constant ADDRESS_LENGTH = 20;
    uint256 private constant SPECIAL_CHARS_LOOKUP =
        (1 << 0x08) | // backspace
            (1 << 0x09) | // tab
            (1 << 0x0a) | // newline
            (1 << 0x0c) | // form feed
            (1 << 0x0d) | // carriage return
            (1 << 0x22) | // double quote
            (1 << 0x5c); // backslash

    /**
     * @dev The `value` string doesn't fit in the specified `length`.
     */
    error StringsInsufficientHexLength(uint256 value, uint256 length);

    /**
     * @dev The string being parsed contains characters that are not in scope of the given base.
     */
    error StringsInvalidChar();

    /**
     * @dev The string being parsed is not a properly formatted address.
     */
    error StringsInvalidAddressFormat();

    /**
     * @dev Converts a `uint256` to its ASCII `string` decimal representation.
     */
    function toString(uint256 value) internal pure returns (string memory) {
        unchecked {
            uint256 length = Math.log10(value) + 1;
            string memory buffer = new string(length);
            uint256 ptr;
            assembly ("memory-safe") {
                ptr := add(add(buffer, 0x20), length)
            }
            while (true) {
                ptr--;
                assembly ("memory-safe") {
                    mstore8(ptr, byte(mod(value, 10), HEX_DIGITS))
                }
                value /= 10;
                if (value == 0) break;
            }
            return buffer;
        }
    }

    /**
     * @dev Converts a `int256` to its ASCII `string` decimal representation.
     */
    function toStringSigned(int256 value) internal pure returns (string memory) {
        return string.concat(value < 0 ? "-" : "", toString(SignedMath.abs(value)));
    }

    /**
     * @dev Converts a `uint256` to its ASCII `string` hexadecimal representation.
     */
    function toHexString(uint256 value) internal pure returns (string memory) {
        unchecked {
            return toHexString(value, Math.log256(value) + 1);
        }
    }

    /**
     * @dev Converts a `uint256` to its ASCII `string` hexadecimal representation with fixed length.
     */
    function toHexString(uint256 value, uint256 length) internal pure returns (string memory) {
        uint256 localValue = value;
        bytes memory buffer = new bytes(2 * length + 2);
        buffer[0] = "0";
        buffer[1] = "x";
        for (uint256 i = 2 * length + 1; i > 1; --i) {
            buffer[i] = HEX_DIGITS[localValue & 0xf];
            localValue >>= 4;
        }
        if (localValue != 0) {
            revert StringsInsufficientHexLength(value, length);
        }
        return string(buffer);
    }

    /**
     * @dev Converts an `address` with fixed length of 20 bytes to its not checksummed ASCII `string` hexadecimal
     * representation.
     */
    function toHexString(address addr) internal pure returns (string memory) {
        return toHexString(uint256(uint160(addr)), ADDRESS_LENGTH);
    }

    /**
     * @dev Converts an `address` with fixed length of 20 bytes to its checksummed ASCII `string` hexadecimal
     * representation, according to EIP-55.
     */
    function toChecksumHexString(address addr) internal pure returns (string memory) {
        bytes memory buffer = bytes(toHexString(addr));

        // hash the hex part of buffer (skip length + 2 bytes, length 40)
        uint256 hashValue;
        assembly ("memory-safe") {
            hashValue := shr(96, keccak256(add(buffer, 0x22), 40))
        }

        for (uint256 i = 41; i > 1; --i) {
            // possible values for buffer[i] are 48 (0) to 57 (9) and 97 (a) to 102 (f)
            if (hashValue & 0xf > 7 && uint8(buffer[i]) > 96) {
                // case shift by xoring with 0x20
                buffer[i] ^= 0x20;
            }
            hashValue >>= 4;
        }
        return string(buffer);
    }

    /**
     * @dev Returns true if the two strings are equal.
     */
    function equal(string memory a, string memory b) internal pure returns (bool) {
        return bytes(a).length == bytes(b).length && keccak256(bytes(a)) == keccak256(bytes(b));
    }

    /**
     * @dev Parse a decimal string and returns the value as a `uint256`.
     *
     * Requirements:
     * - The string must be formatted as `[0-9]*`
     * - The result must fit into an `uint256` type
     */
    function parseUint(string memory input) internal pure returns (uint256) {
        return parseUint(input, 0, bytes(input).length);
    }

    /**
     * @dev Variant of {parseUint-string} that parses a substring of `input` located between position `begin` (included) and
     * `end` (excluded).
     *
     * Requirements:
     * - The substring must be formatted as `[0-9]*`
     * - The result must fit into an `uint256` type
     */
   

Tags:
ERC20, ERC721, ERC165, Multisig, Mintable, Burnable, Non-Fungible, Swap, Upgradeable, Multi-Signature, Factory|addr:0xbf8dee3a9f62849d340e85911991d569d41aee7a|verified:true|block:23535535|tx:0x56096698d288c0fe8cbd21c60481a14b59116d69ab96e2622d954b681dd87edb|first_check:1759993980

Submitted on: 2025-10-09 09:13:00

Comments

Log in to comment.

No comments yet.