AssetVault

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/2025.09.22 T76/AssetVault.sol": {
      "content": "// SPDX-License-Identifier: MIT\r
pragma solidity ^0.8.30;\r
\r
import "@openzeppelin/contracts/access/Ownable.sol";\r
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";\r
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";\r
\r
contract AssetVault is Ownable, ReentrancyGuard {\r
\r
    string public constant VERSION = "1.0.1";\r
\r
    // Error definitions\r
    error InvalidZeroAddress();\r
    error NotAuthorized(address user, uint8 requiredRole);\r
    error InvalidZeroAmount();\r
    error EmptyTransactionHash();\r
    error UnauthorizedCaller(address caller);\r
    error InsufficientAssetBalance(uint256 requiredETH, uint256 availableETH, uint256 requiredGPT, uint256 availableGPT, uint256 requiredBTC, uint256 availableBTC);\r
    error InvalidRequestId(uint256 requestId);\r
    error RequestAlreadyProcessed(uint256 requestId);\r
    error InvalidParameters();\r
    error MissingTransactionHash(string assetType);\r
    error TransactionHashAlreadyUsed(string transactionHash);\r
    error ZeroWithdrawalAmount();\r
    error InsufficientBalance(string assetType);\r
    error InvalidETHReceiverAddress();\r
    error InvalidGPTReceiverAddress();\r
    error InvalidBTCReceiverAddress();\r
    error AddressAlreadyExists();\r
    error AddressNotFound();\r
    error InvalidAssetType();\r
\r
    uint256 private constant MAX_REQUEST_LIMIT = 100;\r
\r
    // Minimum amount thresholds\r
    uint256 public minEthTxAmount = 1000000000000000;   // 0.001 ETH (wei, 10^-18)\r
    uint256 public minGptTxAmount = 1000000000000000;   // 0.001 GPT (in 10^-18 units)\r
    uint256 public minBtcTxAmount = 6000;               // 0.00006 BTC (in satoshi)\r
\r
    // Role constants - Using bitmasks\r
    uint8 private constant ROLE_RECORDER = 1;\r
    uint8 private constant ROLE_PROCESSOR = 2;\r
\r
    // Asset type enumeration\r
    enum AssetType {ETH, GPT, BTC}\r
\r
    // Locked asset structure\r
    struct LockedAssets {\r
        uint256 totalETH;           // Total ETH amount (in wei)\r
        uint256 totalGPT;           // Total GPT amount (in 10^-18 units)\r
        uint256 totalBTC;           // Total BTC amount (in satoshi)\r
    }\r
\r
    // Withdrawal request structure\r
    struct WithdrawalRequest {\r
        address user;               // User address\r
        address ethReceiver;        // ETH receiving address\r
        address gptReceiver;        // GPT receiving address\r
\r
        uint40 requestTimestamp;    // Request timestamp\r
        uint40 processedTimestamp;  // Processing timestamp\r
        bool processed;             // Whether processed\r
\r
        uint256 assetBurned;        // Amount of Asset burned\r
        uint256 ethAmount;          // Withdrawable ETH amount (in wei)\r
        uint256 gptAmount;          // Withdrawable GPT amount (in 10^-18 units)\r
        uint256 btcAmount;          // Withdrawable BTC amount (in satoshi)\r
\r
        string btcReceiver;         // BTC receiving address as string\r
    }\r
\r
    // Asset token interface\r
    IERC20 public assetToken;\r
\r
    // Current total assets\r
    LockedAssets public lockedAssets;\r
\r
    // Multi-address support\r
    mapping(AssetType => address[]) public ethAssetLockAddresses; // ETH and GPT\r
    string[] public btcAssetLockAddresses;                        // BTC\r
\r
    // Unified role management\r
    mapping(address => uint8) public userRoles;\r
\r
    mapping(uint8 => address[]) private roleHolders;\r
    mapping(uint8 => mapping(address => uint256)) private roleIndices;\r
\r
    // Track used transaction hashes\r
    mapping(bytes32 => bool) public usedTransactionHashes;\r
\r
    // Store requests by requestId\r
    mapping(uint256 => WithdrawalRequest) public withdrawalRequests;\r
\r
    // Ordered list of all request IDs for enumeration\r
    uint256[] public allRequestIds;\r
\r
    // User's withdrawal request IDs\r
    mapping(address => uint256[]) public userWithdrawalRequests;\r
\r
    // Events defined with indexed fields for efficient filtering\r
    event AssetDeposited(\r
        address indexed recorder,\r
        AssetType assetType,\r
        uint256 amount,\r
        string transactionHash,\r
        uint256 timestamp\r
    );\r
\r
    event RequestCreated(\r
        address indexed user,\r
        uint256 indexed requestId,\r
        uint256 assetBurned,\r
        uint256 ethAmount,          // In wei\r
        uint256 gptAmount,          // In 10^-18 units\r
        uint256 btcAmount,          // In satoshi\r
        address ethReceiver,        // ETH receiving address\r
        address gptReceiver,        // GPT receiving address\r
        string btcReceiver,         // BTC receiving address\r
        uint256 timestamp\r
    );\r
\r
    event RequestProcessed(\r
        address indexed user,\r
        uint256 indexed requestId,\r
        string ethTxHash,\r
        string gptTxHash,\r
        string btcTxHash,\r
        uint256 timestamp\r
    );\r
\r
    event RoleChanged(address indexed user, uint8 role, bool enabled, uint256 timestamp);\r
\r
    event TokenRescued(address indexed token, address indexed to, uint256 amount, uint256 timestamp);\r
\r
    // New event for threshold updates\r
    event MinTxAmountsUpdated(\r
        uint256 minEthTxAmount,\r
        uint256 minGptTxAmount,\r
        uint256 minBtcTxAmount,\r
        uint256 timestamp\r
    );\r
\r
    event TransactionHashUsed(string transactionHash, address user, uint256 timestamp);\r
\r
    // New event for asset balance adjustments\r
    event AssetBalanceAdjusted(\r
        address indexed operator,\r
        int256 ethDelta,\r
        int256 gptDelta,\r
        int256 btcDelta,\r
        uint256 newEthBalance,\r
        uint256 newGptBalance,\r
        uint256 newBtcBalance,\r
        string reason,\r
        uint256 timestamp\r
    );\r
\r
    // New events for lock address management\r
    event LockAddressAdded(AssetType indexed assetType, address ethAddress, string btcAddress);\r
    event LockAddressRemoved(AssetType indexed assetType, address ethAddress, string btcAddress);\r
\r
    // Modifiers\r
    modifier onlyRole(uint8 role) {\r
        if (!hasRole(msg.sender, role)) revert NotAuthorized(msg.sender, role);\r
        _;\r
    }\r
\r
    /**\r
     * @dev Constructor\r
     * @param _assetToken Asset token address\r
     */\r
    constructor(address _assetToken) Ownable(msg.sender) ReentrancyGuard() {\r
        if (_assetToken == address(0)) revert InvalidZeroAddress();\r
        assetToken = IERC20(_assetToken);\r
    }\r
\r
    /**\r
     * @dev Add an ETH lock address\r
     * @param ethAddress Ethereum address for ETH\r
     */\r
    function addEthLockAddress(address ethAddress) external onlyOwner {\r
        _addEthLockAddress(AssetType.ETH, ethAddress);\r
    }\r
\r
    /**\r
     * @dev Add a GPT lock address\r
     * @param ethAddress Ethereum address for GPT\r
     */\r
    function addGptLockAddress(address ethAddress) external onlyOwner {\r
        _addEthLockAddress(AssetType.GPT, ethAddress);\r
    }\r
\r
    /**\r
     * @dev Add a BTC lock address\r
     * @param btcAddress BTC address\r
     */\r
    function addBtcLockAddress(string calldata btcAddress) external onlyOwner {\r
        _addBtcLockAddress(btcAddress);\r
    }\r
\r
    /**\r
     * @dev Internal helper to add an ETH-type lock address\r
     */\r
    function _addEthLockAddress(AssetType assetType, address ethAddress) internal {\r
        if (ethAddress == address(0)) revert InvalidZeroAddress();\r
\r
        // Check if address already exists\r
        address[] storage addresses = ethAssetLockAddresses[assetType];\r
        for (uint i = 0; i < addresses.length; i++) {\r
            if (addresses[i] == ethAddress) revert AddressAlreadyExists();\r
        }\r
\r
        // Add address\r
        addresses.push(ethAddress);\r
\r
        emit LockAddressAdded(assetType, ethAddress, "");\r
    }\r
\r
    /**\r
     * @dev Internal helper to add a BTC lock address\r
     */\r
    function _addBtcLockAddress(string calldata btcAddress) internal {\r
        if (!isValidBTCAddress(btcAddress)) revert InvalidParameters();\r
\r
        // Check if address already exists\r
        for (uint i = 0; i < btcAssetLockAddresses.length; i++) {\r
            if (keccak256(bytes(btcAssetLockAddresses[i])) == keccak256(bytes(btcAddress)))\r
                revert AddressAlreadyExists();\r
        }\r
\r
        // Add address\r
        btcAssetLockAddresses.push(btcAddress);\r
\r
        emit LockAddressAdded(AssetType.BTC, address(0), btcAddress);\r
    }\r
\r
    /**\r
     * @dev Remove an ETH lock address\r
     * @param ethAddress Ethereum address to remove\r
     */\r
    function removeEthLockAddress(address ethAddress) external onlyOwner {\r
        _removeEthLockAddress(AssetType.ETH, ethAddress);\r
    }\r
\r
    /**\r
     * @dev Remove a GPT lock address\r
     * @param ethAddress Ethereum address to remove\r
     */\r
    function removeGptLockAddress(address ethAddress) external onlyOwner {\r
        _removeEthLockAddress(AssetType.GPT, ethAddress);\r
    }\r
\r
    /**\r
     * @dev Remove a BTC lock address\r
     * @param btcAddress BTC address to remove\r
     */\r
    function removeBtcLockAddress(string calldata btcAddress) external onlyOwner {\r
        _removeBtcLockAddress(btcAddress);\r
    }\r
\r
    /**\r
     * @dev Internal helper to remove an ETH-type lock address\r
     */\r
    function _removeEthLockAddress(AssetType assetType, address ethAddress) internal {\r
        address[] storage addresses = ethAssetLockAddresses[assetType];\r
\r
        bool found = false;\r
        uint256 index;\r
\r
        for (uint i = 0; i < addresses.length; i++) {\r
            if (addresses[i] == ethAddress) {\r
                found = true;\r
                index = i;\r
                break;\r
            }\r
        }\r
\r
        if (!found) revert AddressNotFound();\r
\r
        // Remove address - move last address to the position being deleted, then pop\r
        if (index != addresses.length - 1) {\r
            addresses[index] = addresses[addresses.length - 1];\r
        }\r
        addresses.pop();\r
\r
        emit LockAddressRemoved(assetType, ethAddress, "");\r
    }\r
\r
    /**\r
     * @dev Internal helper to remove a BTC lock address\r
     */\r
    function _removeBtcLockAddress(string calldata btcAddress) internal {\r
        bool found = false;\r
        uint256 index;\r
\r
        for (uint i = 0; i < btcAssetLockAddresses.length; i++) {\r
            if (keccak256(bytes(btcAssetLockAddresses[i])) == keccak256(bytes(btcAddress))) {\r
                found = true;\r
                index = i;\r
                break;\r
            }\r
        }\r
\r
        if (!found) revert AddressNotFound();\r
\r
        // Remove address\r
        if (index != btcAssetLockAddresses.length - 1) {\r
            btcAssetLockAddresses[index] = btcAssetLockAddresses[btcAssetLockAddresses.length - 1];\r
        }\r
        btcAssetLockAddresses.pop();\r
\r
        emit LockAddressRemoved(AssetType.BTC, address(0), btcAddress);\r
    }\r
\r
    /**\r
     * @dev Get all lock addresses for all asset types at once\r
     */\r
    function getAllLockAddresses() external view returns (\r
        address[] memory ethAddresses,\r
        address[] memory gptAddresses,\r
        string[] memory btcAddresses\r
    ) {\r
        return (\r
            ethAssetLockAddresses[AssetType.ETH],\r
            ethAssetLockAddresses[AssetType.GPT],\r
            btcAssetLockAddresses\r
        );\r
    }\r
\r
    /**\r
     * @dev Set minimum transaction amount thresholds\r
     * @param _minEthTxAmount Minimum ETH amount that requires a transaction hash (wei)\r
     * @param _minGptTxAmount Minimum GPT amount that requires a transaction hash (10^-18)\r
     * @param _minBtcTxAmount Minimum BTC amount that requires a transaction hash (satoshi)\r
     */\r
    function setMinTxAmounts(uint256 _minEthTxAmount, uint256 _minGptTxAmount, uint256 _minBtcTxAmount) external onlyOwner {\r
        minEthTxAmount = _minEthTxAmount;\r
        minGptTxAmount = _minGptTxAmount;\r
        minBtcTxAmount = _minBtcTxAmount;\r
\r
        emit MinTxAmountsUpdated(\r
            _minEthTxAmount,\r
            _minGptTxAmount,\r
            _minBtcTxAmount,\r
            block.timestamp\r
        );\r
    }\r
\r
    /**\r
     * @dev Adjust locked asset balances by delta amounts\r
     * @param ethDelta Delta for ETH amount (positive to add, negative to subtract)\r
     * @param gptDelta Delta for GPT amount (positive to add, negative to subtract)\r
     * @param btcDelta Delta for BTC amount (positive to add, negative to subtract)\r
     * @param reason Reason for the adjustment\r
     */\r
    function adjustAssetBalances(int256 ethDelta, int256 gptDelta, int256 btcDelta, string calldata reason) external onlyOwner {\r
        // Skip if all deltas are zero\r
        if (ethDelta == 0 && gptDelta == 0 && btcDelta == 0) revert InvalidZeroAmount();\r
\r
        // Check for sufficient balances when reducing\r
        if (ethDelta < 0 && uint256(-ethDelta) > lockedAssets.totalETH) {\r
            revert InsufficientBalance("ETH");\r
        }\r
\r
        if (gptDelta < 0 && uint256(-gptDelta) > lockedAssets.totalGPT) {\r
            revert InsufficientBalance("GPT");\r
        }\r
\r
        if (btcDelta < 0 && uint256(-btcDelta) > lockedAssets.totalBTC) {\r
            revert InsufficientBalance("BTC");\r
        }\r
\r
        // Apply adjustments\r
        if (ethDelta != 0) {\r
            if (ethDelta > 0) {\r
                lockedAssets.totalETH += uint256(ethDelta);\r
            } else {\r
                lockedAssets.totalETH -= uint256(-ethDelta);\r
            }\r
        }\r
\r
        if (gptDelta != 0) {\r
            if (gptDelta > 0) {\r
                lockedAssets.totalGPT += uint256(gptDelta);\r
            } else {\r
                lockedAssets.totalGPT -= uint256(-gptDelta);\r
            }\r
        }\r
\r
        if (btcDelta != 0) {\r
            if (btcDelta > 0) {\r
                lockedAssets.totalBTC += uint256(btcDelta);\r
            } else {\r
                lockedAssets.totalBTC -= uint256(-btcDelta);\r
            }\r
        }\r
\r
        // Emit event with deltas and new balances\r
        emit AssetBalanceAdjusted(\r
            msg.sender,\r
            ethDelta,\r
            gptDelta,\r
            btcDelta,\r
            lockedAssets.totalETH,\r
            lockedAssets.totalGPT,\r
            lockedAssets.totalBTC,\r
            reason,\r
            block.timestamp\r
        );\r
    }\r
\r
    /**\r
     * @dev Check if user has specified role\r
     */\r
    function hasRole(address user, uint8 role) public view returns (bool) {\r
        return (userRoles[user] & role != 0) || user == owner();\r
    }\r
\r
    /**\r
     * @dev Set user role\r
     */\r
    function setRole(address user, uint8 role, bool enabled) external onlyOwner {\r
        if (user == address(0)) revert InvalidZeroAddress();\r
\r
        // Cache current role state\r
        uint8 currentRoles = userRoles[user];\r
        bool hasRoleBefore = (currentRoles & role != 0);\r
\r
        // Early return to save gas\r
        if (hasRoleBefore == enabled) return;\r
\r
        if (enabled) {\r
            // Update role bitmap\r
            userRoles[user] = currentRoles | role;\r
\r
            // Add to role holders list if not already there\r
            uint256 index = roleIndices[role][user];\r
            if (index == 0) {\r
                roleHolders[role].push(user);\r
                roleIndices[role][user] = roleHolders[role].length;\r
            }\r
        } else {\r
            // Update role bitmap\r
            userRoles[user] = currentRoles & ~role;\r
\r
            // Remove from role holders list\r
            uint256 index = roleIndices[role][user];\r
            if (index > 0) {\r
                unchecked {\r
                    index--; // Convert to 0-based index\r
                    uint256 lastIndex = roleHolders[role].length - 1;\r
\r
                    // If not the last element, replace with the last element\r
                    if (index != lastIndex) {\r
                        address lastAddress = roleHolders[role][lastIndex];\r
                        roleHolders[role][index] = lastAddress;\r
                        roleIndices[role][lastAddress] = index + 1;\r
                    }\r
\r
                    // Remove last element and clear index\r
                    roleHolders[role].pop();\r
                    roleIndices[role][user] = 0;\r
                }\r
            }\r
        }\r
\r
        emit RoleChanged(user, role, enabled, block.timestamp);\r
    }\r
\r
    /**\r
     * @dev Get users with specific role\r
     */\r
    function getUsersWithRole(uint8 role) public view returns (address[] memory) {\r
        return roleHolders[role];\r
    }\r
\r
    /**\r
     * @dev Get all operators (ROLE_RECORDER)\r
     */\r
    function getOperators() external view returns (address[] memory) {\r
        return getUsersWithRole(ROLE_RECORDER);\r
    }\r
\r
    /**\r
     * @dev Get all processors (ROLE_PROCESSOR)\r
     */\r
    function getProcessors() external view returns (address[] memory) {\r
        return getUsersWithRole(ROLE_PROCESSOR);\r
    }\r
\r
    /**\r
     * @dev Check if request exists\r
     */\r
    function requestExists(uint256 requestId) public view returns (bool) {\r
        return withdrawalRequests[requestId].requestTimestamp > 0;\r
    }\r
\r
    /**\r
     * @dev Record asset deposit\r
     */\r
    function depositAsset(AssetType assetType, uint256 amount, string calldata transactionHash) external onlyRole(ROLE_RECORDER) {\r
        if (amount == 0) revert InvalidZeroAmount();\r
        _validateAndRecordTxHash(transactionHash, "");\r
\r
        // Use unchecked to update state\r
        unchecked {\r
        // Update locked assets based on asset type - using concise conditionals\r
            if (assetType == AssetType.ETH) {\r
                lockedAssets.totalETH += amount;\r
            } else if (assetType == AssetType.GPT) {\r
                lockedAssets.totalGPT += amount;\r
            } else {\r
                // Default to BTC type\r
                lockedAssets.totalBTC += amount;\r
            }\r
        }\r
\r
        // Emit event\r
        emit AssetDeposited(\r
            msg.sender,\r
            assetType,\r
            amount,\r
            transactionHash,\r
            block.timestamp\r
        );\r
    }\r
\r
    /**\r
     * @dev Validate BTC address format\r
     * Support for both mainnet and testnet addresses\r
     */\r
    function isValidBTCAddress(string memory btcAddr) internal pure returns (bool) {\r
        bytes memory b = bytes(btcAddr);\r
\r
        // Length check - early return saves gas\r
        uint256 len = b.length;\r
        if (len < 26 || len > 90) return false;\r
\r
        // Single character lookup\r
        bytes1 firstChar = b[0];\r
\r
        // Check against valid first characters\r
        return (\r
            firstChar == 0x31 || // '1' - Mainnet P2PKH\r
            firstChar == 0x33 || // '3' - Mainnet P2SH\r
            firstChar == 0x62 || // 'b' - Mainnet SegWit\r
            firstChar == 0x6D || // 'm' - Testnet P2PKH\r
            firstChar == 0x6E || // 'n' - Testnet P2PKH\r
            firstChar == 0x32 || // '2' - Testnet P2SH\r
            firstChar == 0x74    // 't' - Testnet SegWit\r
        );\r
    }\r
\r
    /**\r
     * @dev Calculate withdrawal amounts and check thresholds\r
     */\r
    function calculateWithdrawalAmounts(uint256 assetAmount) public view returns (uint256 ethToWithdraw, uint256 gptToWithdraw, uint256 btcToWithdraw) {\r
        // Cache asset state in single read operations\r
        uint256 supply = assetToken.totalSupply();\r
\r
        if (supply == 0) return (0, 0, 0);\r
\r
        uint256 totalETH = lockedAssets.totalETH;\r
        uint256 totalGPT = lockedAssets.totalGPT;\r
        uint256 totalBTC = lockedAssets.totalBTC;\r
\r
        // Calculate withdrawal amounts with combined operations\r
        unchecked {\r
            // Direct ratio calculations with simplified conditions\r
            ethToWithdraw = totalETH > 0 ? (totalETH * 1e18 / supply * assetAmount) / 1e18 : 0;\r
            gptToWithdraw = totalGPT > 0 ? (totalGPT * 1e18 / supply * assetAmount) / 1e18 : 0;\r
            btcToWithdraw = totalBTC > 0 ? (totalBTC * 1e18 / supply * assetAmount) / 1e18 : 0;\r
\r
            // Truncate GPT to 4 decimal places\r
            gptToWithdraw = (gptToWithdraw / 1e14) * 1e14;\r
\r
            // Apply minimum thresholds with direct assignment\r
            if (ethToWithdraw < minEthTxAmount) ethToWithdraw = 0;\r
            if (gptToWithdraw < minGptTxAmount) gptToWithdraw = 0;\r
            if (btcToWithdraw < minBtcTxAmount) btcToWithdraw = 0;\r
        }\r
\r
        return (ethToWithdraw, gptToWithdraw, btcToWithdraw);\r
    }\r
\r
    /**\r
     * @dev Process asset burn and create withdrawal request\r
     * @param user User address who burned asset tokens\r
     * @param assetAmount Amount of asset tokens burned\r
     * @param requestId request ID to identify this withdrawal\r
     * @param ethReceiver Address to receive ETH\r
     * @param gptReceiver Address to receive GPT tokens\r
     * @param btcReceiver BTC address to receive Bitcoin\r
     */\r
    function createWithdrawalRequest(\r
        address user,\r
        uint256 assetAmount,\r
        uint256 requestId,\r
        address ethReceiver,\r
        address gptReceiver,\r
        string memory btcReceiver\r
    ) external nonReentrant {\r
        // Security checks\r
        if (msg.sender != address(assetToken)) revert UnauthorizedCaller(msg.sender);\r
        if (requestExists(requestId)) revert RequestAlreadyProcessed(requestId);\r
\r
        // Check if total supply is zero\r
        if (assetToken.totalSupply() == 0) revert InvalidParameters();\r
\r
        // Calculate withdrawals and update balances\r
        (uint256 ethToWithdraw, uint256 gptToWithdraw, uint256 btcToWithdraw) = calculateWithdrawalAmounts(assetAmount);\r
\r
        // Check if all withdrawal amounts are zero\r
        if (ethToWithdraw == 0 && gptToWithdraw == 0 && btcToWithdraw == 0) {\r
            revert ZeroWithdrawalAmount();\r
        }\r
\r
        if (ethToWithdraw > 0 && ethReceiver == address(0)) {\r
            revert InvalidETHReceiverAddress();\r
        }\r
\r
        if (gptToWithdraw > 0 && gptReceiver == address(0)) {\r
            revert InvalidGPTReceiverAddress();\r
        }\r
\r
        if (btcToWithdraw > 0 && !isValidBTCAddress(btcReceiver)) {\r
            revert InvalidBTCReceiverAddress();\r
        }\r
\r
        uint256 totalETH = lockedAssets.totalETH;\r
        uint256 totalGPT = lockedAssets.totalGPT;\r
        uint256 totalBTC = lockedAssets.totalBTC;\r
\r
        // Combined balance check in one condition\r
        if (totalETH < ethToWithdraw || totalGPT < gptToWithdraw || totalBTC < btcToWithdraw) {\r
            revert InsufficientAssetBalance(\r
                ethToWithdraw, totalETH,\r
                gptToWithdraw, totalGPT,\r
                btcToWithdraw, totalBTC\r
            );\r
        }\r
\r
        unchecked {\r
            lockedAssets.totalETH = totalETH - ethToWithdraw;\r
            lockedAssets.totalGPT = totalGPT - gptToWithdraw;\r
            lockedAssets.totalBTC = totalBTC - btcToWithdraw;\r
        }\r
\r
        withdrawalRequests[requestId] = WithdrawalRequest({\r
            user: user,\r
            ethReceiver: ethToWithdraw > 0 ? ethReceiver : address(0),\r
            gptReceiver: gptToWithdraw > 0 ? gptReceiver : address(0),\r
            requestTimestamp: uint40(block.timestamp),\r
            processedTimestamp: 0,\r
            processed: false,\r
            assetBurned: assetAmount,\r
            ethAmount: ethToWithdraw,\r
            gptAmount: gptToWithdraw,\r
            btcAmount: btcToWithdraw,\r
            btcReceiver: btcToWithdraw > 0 ? btcReceiver : ""\r
        });\r
\r
        unchecked {\r
            allRequestIds.push(requestId);\r
            userWithdrawalRequests[user].push(requestId);\r
        }\r
\r
        emit RequestCreated(\r
            user, requestId, assetAmount,\r
            ethToWithdraw, gptToWithdraw, btcToWithdraw,\r
            ethToWithdraw > 0 ? ethReceiver : address(0),\r
            gptToWithdraw > 0 ? gptReceiver : address(0),\r
            btcToWithdraw > 0 ? btcReceiver : "",\r
            block.timestamp\r
        );\r
    }\r
\r
    /**\r
     * @dev Validate and record transaction hash usage\r
     */\r
    function _validateAndRecordTxHash(string calldata transactionHash, string memory assetType) internal {\r
        if (bytes(transactionHash).length == 0) {\r
            if (bytes(assetType).length > 0) {\r
                revert MissingTransactionHash(assetType);\r
            } else {\r
                revert EmptyTransactionHash();\r
            }\r
        }\r
\r
        bytes32 txHashDigest = keccak256(abi.encodePacked(transactionHash));\r
\r
        if (usedTransactionHashes[txHashDigest]) {\r
            revert TransactionHashAlreadyUsed(transactionHash);\r
        }\r
\r
        usedTransactionHashes[txHashDigest] = true;\r
\r
        emit TransactionHashUsed(transactionHash, msg.sender, block.timestamp);\r
    }\r
\r
    /**\r
     * @dev Mark withdrawal request as processed\r
     */\r
    function processWithdrawalRequest(\r
        uint256 requestId,\r
        string calldata ethTxHash,\r
        string calldata gptTxHash,\r
        string calldata btcTxHash\r
    ) external onlyRole(ROLE_PROCESSOR) {\r
        WithdrawalRequest storage request = withdrawalRequests[requestId];\r
\r
        if (request.user == address(0)) revert InvalidRequestId(requestId);\r
        if (request.processed) revert RequestAlreadyProcessed(requestId);\r
\r
        if (request.ethAmount > 0) {\r
            _validateAndRecordTxHash(ethTxHash, "ETH");\r
        }\r
\r
        if (request.gptAmount > 0) {\r
            _validateAndRecordTxHash(gptTxHash, "GPT");\r
        }\r
\r
        if (request.btcAmount > 0) {\r
            _validateAndRecordTxHash(btcTxHash, "BTC");\r
        }\r
\r
        request.processed = true;\r
        request.processedTimestamp = uint40(block.timestamp);\r
\r
        emit RequestProcessed(\r
            request.user,\r
            requestId,\r
            ethTxHash,\r
            gptTxHash,\r
            btcTxHash,\r
            block.timestamp\r
        );\r
    }\r
\r
    /**\r
     * @dev Get a range of request IDs\r
     */\r
    function getRequestIds(uint256 start, uint256 limit, bool processedOnly) external view returns (uint256[] memory) {\r
        uint256 totalRequests = allRequestIds.length;\r
        uint256[] memory result;\r
\r
        if (limit > MAX_REQUEST_LIMIT) {\r
            limit = MAX_REQUEST_LIMIT;\r
        }\r
\r
        if (start >= totalRequests || limit == 0) {\r
            return new uint256[](0);\r
        }\r
\r
        uint256 availableItems;\r
        unchecked {\r
            availableItems = totalRequests - start;\r
        }\r
        uint256 maxItems = availableItems < limit ? availableItems : limit;\r
\r
        if (!processedOnly) {\r
            result = new uint256[](maxItems);\r
\r
            unchecked {\r
                for (uint256 i = 0; i < maxItems; i++) {\r
                    result[i] = allRequestIds[start + i];\r
                }\r
            }\r
            return result;\r
        }\r
\r
        uint256 validCount = 0;\r
        uint256 endIndex = start + maxItems < totalRequests ? start + maxItems : totalRequests;\r
\r
        unchecked {\r
            for (uint256 i = start; i < endIndex; i++) {\r
                if (withdrawalRequests[allRequestIds[i]].processed) {\r
                    validCount++;\r
                }\r
            }\r
        }\r
\r
        result = new uint256[](validCount);\r
\r
        if (validCount > 0) {\r
            uint256 resultIndex = 0;\r
\r
            unchecked {\r
                for (uint256 i = start; i < endIndex && resultIndex < validCount; i++) {\r
                    uint256 reqId = allRequestIds[i];\r
                    if (withdrawalRequests[reqId].processed) {\r
                        result[resultIndex++] = reqId;\r
                    }\r
                }\r
            }\r
        }\r
\r
        return result;\r
    }\r
\r
    /**\r
     * @dev Get total number of requests\r
     */\r
    function getRequestCount() external view returns (uint256) {\r
        return allRequestIds.length;\r
    }\r
\r
    /**\r
     * @dev Get user's withdrawal request list\r
     */\r
    function getUserRequests(address user, uint256 start, uint256 limit) external view returns (uint256[] memory) {\r
        if (limit > MAX_REQUEST_LIMIT) {\r
            limit = MAX_REQUEST_LIMIT;\r
        }\r
\r
        uint256[] memory userRequests = userWithdrawalRequests[user];\r
        uint256 totalUserRequests = userRequests.length;\r
\r
        if (start >= totalUserRequests || limit == 0) {\r
            return new uint256[](0);\r
        }\r
\r
        uint256 availableItems;\r
        unchecked {\r
            availableItems = totalUserRequests - start;\r
        }\r
        uint256 maxItems = availableItems < limit ? availableItems : limit;\r
\r
        uint256[] memory result = new uint256[](maxItems);\r
\r
        unchecked {\r
            for (uint256 i = 0; i < maxItems; i++) {\r
                result[i] = userRequests[start + i];\r
            }\r
        }\r
\r
        return result;\r
    }\r
\r
    /**\r
     * @dev Get user request count\r
     */\r
    function getUserRequestCount(address user) external view returns (uint256) {\r
        return userWithdrawalRequests[user].length;\r
    }\r
\r
    /**\r
     * @dev Get request details\r
     */\r
    function getRequest(uint256 requestId) external view returns (\r
        address user,\r
        uint256 assetBurned,\r
        uint256 ethAmount,\r
        uint256 gptAmount,\r
        uint256 btcAmount,\r
        uint256 requestTimestamp,\r
        bool processed,\r
        uint256 processedTimestamp,\r
        address ethReceiver,\r
        address gptReceiver,\r
        string memory btcReceiver\r
    ) {\r
        WithdrawalRequest memory request = withdrawalRequests[requestId];\r
        if (request.user == address(0)) revert InvalidRequestId(requestId);\r
\r
        return (\r
            request.user,\r
            request.assetBurned,\r
            request.ethAmount,\r
            request.gptAmount,\r
            request.btcAmount,\r
            request.requestTimestamp,\r
            request.processed,\r
            request.processedTimestamp,\r
            request.ethReceiver,\r
            request.gptReceiver,\r
            request.btcReceiver\r
        );\r
    }\r
\r
    /**\r
     * @dev Get current asset ratio\r
     */\r
    function getAssetRatio() external view returns (uint256 ethPerAsset, uint256 gptPerAsset, uint256 btcPerAsset) {\r
        uint256 supply = assetToken.totalSupply();\r
\r
        if (supply == 0) return (0, 0, 0);\r
\r
        uint256 totalETH = lockedAssets.totalETH;\r
        uint256 totalGPT = lockedAssets.totalGPT;\r
        uint256 totalBTC = lockedAssets.totalBTC;\r
\r
        unchecked {\r
            ethPerAsset = totalETH > 0 ? (totalETH * 1e18) / supply : 0;\r
            gptPerAsset = totalGPT > 0 ? (totalGPT * 1e18) / supply : 0;\r
            btcPerAsset = totalBTC > 0 ? (totalBTC * 1e18) / supply : 0;\r
        }\r
    }\r
\r
    /**\r
     * @dev Get total locked assets\r
     */\r
    function getLockedAssets() external view returns (uint256 totalETH, uint256 totalGPT, uint256 totalBTC) {\r
        return (\r
            lockedAssets.totalETH,\r
            lockedAssets.totalGPT,\r
            lockedAssets.totalBTC\r
        );\r
    }\r
\r
    /**\r
     * @dev Rescue tokens\r
     */\r
    function rescueToken(IERC20 token, uint256 amount) external onlyOwner {\r
        if (address(token) == address(0)) revert InvalidZeroAddress();\r
        if (amount == 0) revert InvalidZeroAmount();\r
\r
        address ownerAddr = owner();\r
        token.transfer(ownerAddr, amount);\r
\r
        emit TokenRescued(address(token), ownerAddr, amount, block.timestamp);\r
    }\r
}"
    },
    "@openzeppelin/contracts/token/ERC20/IERC20.sol": {
      "content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.1.0) (token/ERC20/IERC20.sol)

pragma solidity ^0.8.20;

/**
 * @dev Interface of the ERC-20 standard as defined in the ERC.
 */
interface IERC20 {
    /**
     * @dev Emitted when `value` tokens are moved from one account (`from`) to
     * another (`to`).
     *
     * Note that `value` may be zero.
     */
    event Transfer(address indexed from, address indexed to, uint256 value);

    /**
     * @dev Emitted when the allowance of a `spender` for an `owner` is set by
     * a call to {approve}. `value` is the new allowance.
     */
    event Approval(address indexed owner, address indexed spender, uint256 value);

    /**
     * @dev Returns the value of tokens in existence.
     */
    function totalSupply() external view returns (uint256);

    /**
     * @dev Returns the value of tokens owned by `account`.
     */
    function balanceOf(address account) external view returns (uint256);

    /**
     * @dev Moves a `value` amount of tokens from the caller's account to `to`.
     *
     * Returns a boolean value indicating whether the operation succeeded.
     *
     * Emits a {Transfer} event.
     */
    function transfer(address to, uint256 value) external returns (bool);

    /**
     * @dev Returns the remaining number of tokens that `spender` will be
     * allowed to spend on behalf of `owner` through {transferFrom}. This is
     * zero by default.
     *
     * This value changes when {approve} or {transferFrom} are called.
     */
    function allowance(address owner, address spender) external view returns (uint256);

    /**
     * @dev Sets a `value` amount of tokens as the allowance of `spender` over the
     * caller's tokens.
     *
     * Returns a boolean value indicating whether the operation succeeded.
     *
     * IMPORTANT: Beware that changing an allowance with this method brings the risk
     * that someone may use both the old and the new allowance by unfortunate
     * transaction ordering. One possible solution to mitigate this race
     * condition is to first reduce the spender's allowance to 0 and set the
     * desired value afterwards:
     * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
     *
     * Emits an {Approval} event.
     */
    function approve(address spender, uint256 value) external returns (bool);

    /**
     * @dev Moves a `value` amount of tokens from `from` to `to` using the
     * allowance mechanism. `value` is then deducted from the caller's
     * allowance.
     *
     * Returns a boolean value indicating whether the operation succeeded.
     *
     * Emits a {Transfer} event.
     */
    function transferFrom(address from, address to, uint256 value) external returns (bool);
}
"
    },
    "@openzeppelin/contracts/security/ReentrancyGuard.sol": {
      "content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.9.0) (security/ReentrancyGuard.sol)

pragma solidity ^0.8.0;

/**
 * @dev Contract module that helps prevent reentrant calls to a function.
 *
 * Inheriting from `ReentrancyGuard` will make the {nonReentrant} modifier
 * available, which can be applied to functions to make sure there are no nested
 * (reentrant) calls to them.
 *
 * Note that because there is a single `nonReentrant` guard, functions marked as
 * `nonReentrant` may not call one another. This can be worked around by making
 * those functions `private`, and then adding `external` `nonReentrant` entry
 * points to them.
 *
 * TIP: If you would like to learn more about reentrancy and alternative ways
 * to protect against it, check out our blog post
 * https://blog.openzeppelin.com/reentrancy-after-istanbul/[Reentrancy After Istanbul].
 */
abstract contract ReentrancyGuard {
    // Booleans are more expensive than uint256 or any type that takes up a full
    // word because each write operation emits an extra SLOAD to first read the
    // slot's contents, replace the bits taken up by the boolean, and then write
    // back. This is the compiler's defense against contract upgrades and
    // pointer aliasing, and it cannot be disabled.

    // The values being non-zero value makes deployment a bit more expensive,
    // but in exchange the refund on every call to nonReentrant will be lower in
    // amount. Since refunds are capped to a percentage of the total
    // transaction's gas, it is best to keep them low in cases like this one, to
    // increase the likelihood of the full refund coming into effect.
    uint256 private constant _NOT_ENTERED = 1;
    uint256 private constant _ENTERED = 2;

    uint256 private _status;

    constructor() {
        _status = _NOT_ENTERED;
    }

    /**
     * @dev Prevents a contract from calling itself, directly or indirectly.
     * Calling a `nonReentrant` function from another `nonReentrant`
     * function is not supported. It is possible to prevent this from happening
     * by making the `nonReentrant` function external, and making it call a
     * `private` function that does the actual work.
     */
    modifier nonReentrant() {
        _nonReentrantBefore();
        _;
        _nonReentrantAfter();
    }

    function _nonReentrantBefore() private {
        // On the first call to nonReentrant, _status will be _NOT_ENTERED
        require(_status != _ENTERED, "ReentrancyGuard: reentrant call");

        // Any calls to nonReentrant after this point will fail
        _status = _ENTERED;
    }

    function _nonReentrantAfter() private {
        // By storing the original value once again, a refund is triggered (see
        // https://eips.ethereum.org/EIPS/eip-2200)
        _status = _NOT_ENTERED;
    }

    /**
     * @dev Returns true if the reentrancy guard is currently set to "entered", which indicates there is a
     * `nonReentrant` function in the call stack.
     */
    function _reentrancyGuardEntered() internal view returns (bool) {
        return _status == _ENTERED;
    }
}
"
    },
    "@openzeppelin/contracts/access/Ownable.sol": {
      "content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (access/Ownable.sol)

pragma solidity ^0.8.20;

import {Context} from "../utils/Context.sol";

/**
 * @dev Contract module which provides a basic access control mechanism, where
 * there is an account (an owner) that can be granted exclusive access to
 * specific functions.
 *
 * The initial owner is set to the address provided by the deployer. This can
 * later be changed with {transferOwnership}.
 *
 * This module is used through inheritance. It will make available the modifier
 * `onlyOwner`, which can be applied to your functions to restrict their use to
 * the owner.
 */
abstract contract Ownable is Context {
    address private _owner;

    /**
     * @dev The caller account is not authorized to perform an operation.
     */
    error OwnableUnauthorizedAccount(address account);

    /**
     * @dev The owner is not a valid owner account. (eg. `address(0)`)
     */
    error OwnableInvalidOwner(address owner);

    event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);

    /**
     * @dev Initializes the contract setting the address provided by the deployer as the initial owner.
     */
    constructor(address initialOwner) {
        if (initialOwner == address(0)) {
            revert OwnableInvalidOwner(address(0));
        }
        _transferOwnership(initialOwner);
    }

    /**
     * @dev Throws if called by any account other than the owner.
     */
    modifier onlyOwner() {
        _checkOwner();
        _;
    }

    /**
     * @dev Returns the address of the current owner.
     */
    function owner() public view virtual returns (address) {
        return _owner;
    }

    /**
     * @dev Throws if the sender is not the owner.
     */
    function _checkOwner() internal view virtual {
        if (owner() != _msgSender()) {
            revert OwnableUnauthorizedAccount(_msgSender());
        }
    }

    /**
     * @dev Leaves the contract without owner. It will not be possible to call
     * `onlyOwner` functions. Can only be called by the current owner.
     *
     * NOTE: Renouncing ownership will leave the contract without an owner,
     * thereby disabling any functionality that is only available to the owner.
     */
    function renounceOwnership() public virtual onlyOwner {
        _transferOwnership(address(0));
    }

    /**
     * @dev Transfers ownership of the contract to a new account (`newOwner`).
     * Can only be called by the current owner.
     */
    function transferOwnership(address newOwner) public virtual onlyOwner {
        if (newOwner == address(0)) {
            revert OwnableInvalidOwner(address(0));
        }
        _transferOwnership(newOwner);
    }

    /**
     * @dev Transfers ownership of the contract to a new account (`newOwner`).
     * Internal function without access restriction.
     */
    function _transferOwnership(address newOwner) internal virtual {
        address oldOwner = _owner;
        _owner = newOwner;
        emit OwnershipTransferred(oldOwner, newOwner);
    }
}
"
    },
    "@openzeppelin/contracts/utils/Context.sol": {
      "content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.1) (utils/Context.sol)

pragma solidity ^0.8.20;

/**
 * @dev Provides information about the current execution context, including the
 * sender of the transaction and its data. While these are generally available
 * via msg.sender and msg.data, they should not be accessed in such a direct
 * manner, since when dealing with meta-transactions the account sending and
 * paying for execution may not be the actual sender (as far as an application
 * is concerned).
 *
 * This contract is only required for intermediate, library-like contracts.
 */
abstract contract Context {
    function _msgSender() internal view virtual returns (address) {
        return msg.sender;
    }

    function _msgData() internal view virtual returns (bytes calldata) {
        return msg.data;
    }

    function _contextSuffixLength() internal view virtual returns (uint256) {
        return 0;
    }
}
"
    }
  },
  "settings": {
    "optimizer": {
      "enabled": true,
      "runs": 200
    },
    "outputSelection": {
      "*": {
        "*": [
          "evm.bytecode",
          "evm.deployedBytecode",
          "devdoc",
          "userdoc",
          "metadata",
          "abi"
        ]
      }
    },
    "remappings": []
  }
}}

Tags:
ERC20, Multisig, Multi-Signature, Factory|addr:0xd9ead6fa7e21272f477225e43a5e97f96b28a200|verified:true|block:23417905|tx:0xf94d11baf0bd42808dc186b930501bb500dcf65dfed267885b4d574949267a40|first_check:1758538384

Submitted on: 2025-09-22 12:53:05

Comments

Log in to comment.

No comments yet.