ZeniGameVault

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": {
    "@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/token/ERC20/IERC20.sol": {
      "content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (token/ERC20/IERC20.sol)

pragma solidity ^0.8.20;

/**
 * @dev Interface of the ERC20 standard as defined in the EIP.
 */
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/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;
    }
}
"
    },
    "@openzeppelin/contracts/utils/cryptography/ECDSA.sol": {
      "content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (utils/cryptography/ECDSA.sol)

pragma solidity ^0.8.20;

/**
 * @dev Elliptic Curve Digital Signature Algorithm (ECDSA) operations.
 *
 * These functions can be used to verify that a message was signed by the holder
 * of the private keys of a given address.
 */
library ECDSA {
    enum RecoverError {
        NoError,
        InvalidSignature,
        InvalidSignatureLength,
        InvalidSignatureS
    }

    /**
     * @dev The signature derives the `address(0)`.
     */
    error ECDSAInvalidSignature();

    /**
     * @dev The signature has an invalid length.
     */
    error ECDSAInvalidSignatureLength(uint256 length);

    /**
     * @dev The signature has an S value that is in the upper half order.
     */
    error ECDSAInvalidSignatureS(bytes32 s);

    /**
     * @dev Returns the address that signed a hashed message (`hash`) with `signature` or an error. This will not
     * return address(0) without also returning an error description. Errors are documented using an enum (error type)
     * and a bytes32 providing additional information about the error.
     *
     * If no error is returned, then the address can be used for verification purposes.
     *
     * The `ecrecover` EVM precompile allows for malleable (non-unique) signatures:
     * this function rejects them by requiring the `s` value to be in the lower
     * half order, and the `v` value to be either 27 or 28.
     *
     * IMPORTANT: `hash` _must_ be the result of a hash operation for the
     * verification to be secure: it is possible to craft signatures that
     * recover to arbitrary addresses for non-hashed data. A safe way to ensure
     * this is by receiving a hash of the original message (which may otherwise
     * be too long), and then calling {MessageHashUtils-toEthSignedMessageHash} on it.
     *
     * Documentation for signature generation:
     * - with https://web3js.readthedocs.io/en/v1.3.4/web3-eth-accounts.html#sign[Web3.js]
     * - with https://docs.ethers.io/v5/api/signer/#Signer-signMessage[ethers]
     */
    function tryRecover(bytes32 hash, bytes memory signature) internal pure returns (address, RecoverError, bytes32) {
        if (signature.length == 65) {
            bytes32 r;
            bytes32 s;
            uint8 v;
            // ecrecover takes the signature parameters, and the only way to get them
            // currently is to use assembly.
            /// @solidity memory-safe-assembly
            assembly {
                r := mload(add(signature, 0x20))
                s := mload(add(signature, 0x40))
                v := byte(0, mload(add(signature, 0x60)))
            }
            return tryRecover(hash, v, r, s);
        } else {
            return (address(0), RecoverError.InvalidSignatureLength, bytes32(signature.length));
        }
    }

    /**
     * @dev Returns the address that signed a hashed message (`hash`) with
     * `signature`. This address can then be used for verification purposes.
     *
     * The `ecrecover` EVM precompile allows for malleable (non-unique) signatures:
     * this function rejects them by requiring the `s` value to be in the lower
     * half order, and the `v` value to be either 27 or 28.
     *
     * IMPORTANT: `hash` _must_ be the result of a hash operation for the
     * verification to be secure: it is possible to craft signatures that
     * recover to arbitrary addresses for non-hashed data. A safe way to ensure
     * this is by receiving a hash of the original message (which may otherwise
     * be too long), and then calling {MessageHashUtils-toEthSignedMessageHash} on it.
     */
    function recover(bytes32 hash, bytes memory signature) internal pure returns (address) {
        (address recovered, RecoverError error, bytes32 errorArg) = tryRecover(hash, signature);
        _throwError(error, errorArg);
        return recovered;
    }

    /**
     * @dev Overload of {ECDSA-tryRecover} that receives the `r` and `vs` short-signature fields separately.
     *
     * See https://eips.ethereum.org/EIPS/eip-2098[EIP-2098 short signatures]
     */
    function tryRecover(bytes32 hash, bytes32 r, bytes32 vs) internal pure returns (address, RecoverError, bytes32) {
        unchecked {
            bytes32 s = vs & bytes32(0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff);
            // We do not check for an overflow here since the shift operation results in 0 or 1.
            uint8 v = uint8((uint256(vs) >> 255) + 27);
            return tryRecover(hash, v, r, s);
        }
    }

    /**
     * @dev Overload of {ECDSA-recover} that receives the `r and `vs` short-signature fields separately.
     */
    function recover(bytes32 hash, bytes32 r, bytes32 vs) internal pure returns (address) {
        (address recovered, RecoverError error, bytes32 errorArg) = tryRecover(hash, r, vs);
        _throwError(error, errorArg);
        return recovered;
    }

    /**
     * @dev Overload of {ECDSA-tryRecover} that receives the `v`,
     * `r` and `s` signature fields separately.
     */
    function tryRecover(
        bytes32 hash,
        uint8 v,
        bytes32 r,
        bytes32 s
    ) internal pure returns (address, RecoverError, bytes32) {
        // EIP-2 still allows signature malleability for ecrecover(). Remove this possibility and make the signature
        // unique. Appendix F in the Ethereum Yellow paper (https://ethereum.github.io/yellowpaper/paper.pdf), defines
        // the valid range for s in (301): 0 < s < secp256k1n ÷ 2 + 1, and for v in (302): v ∈ {27, 28}. Most
        // signatures from current libraries generate a unique signature with an s-value in the lower half order.
        //
        // If your library generates malleable signatures, such as s-values in the upper range, calculate a new s-value
        // with 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 - s1 and flip v from 27 to 28 or
        // vice versa. If your library also generates signatures with 0/1 for v instead 27/28, add 27 to v to accept
        // these malleable signatures as well.
        if (uint256(s) > 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0) {
            return (address(0), RecoverError.InvalidSignatureS, s);
        }

        // If the signature is valid (and not malleable), return the signer address
        address signer = ecrecover(hash, v, r, s);
        if (signer == address(0)) {
            return (address(0), RecoverError.InvalidSignature, bytes32(0));
        }

        return (signer, RecoverError.NoError, bytes32(0));
    }

    /**
     * @dev Overload of {ECDSA-recover} that receives the `v`,
     * `r` and `s` signature fields separately.
     */
    function recover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) internal pure returns (address) {
        (address recovered, RecoverError error, bytes32 errorArg) = tryRecover(hash, v, r, s);
        _throwError(error, errorArg);
        return recovered;
    }

    /**
     * @dev Optionally reverts with the corresponding custom error according to the `error` argument provided.
     */
    function _throwError(RecoverError error, bytes32 errorArg) private pure {
        if (error == RecoverError.NoError) {
            return; // no error: do nothing
        } else if (error == RecoverError.InvalidSignature) {
            revert ECDSAInvalidSignature();
        } else if (error == RecoverError.InvalidSignatureLength) {
            revert ECDSAInvalidSignatureLength(uint256(errorArg));
        } else if (error == RecoverError.InvalidSignatureS) {
            revert ECDSAInvalidSignatureS(errorArg);
        }
    }
}
"
    },
    "@openzeppelin/contracts/utils/ReentrancyGuard.sol": {
      "content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (utils/ReentrancyGuard.sol)

pragma solidity ^0.8.20;

/**
 * @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;

    /**
     * @dev Unauthorized reentrant call.
     */
    error ReentrancyGuardReentrantCall();

    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
        if (_status == ENTERED) {
            revert ReentrancyGuardReentrantCall();
        }

        // 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;
    }
}
"
    },
    "contract/eth/utils/MessageHashUtils.sol": {
      "content": "// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "../../../utils/MessageHashUtils.sol";
"
    },
    "contract/eth/ZeniGameVault.sol": {
      "content": "// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import "./utils/MessageHashUtils.sol";

/**
 * @title ZeniGameVault
 * @dev Provides deposit management and receipt functionality for zeni tokens on ETH main chain
 * This contract serves as a bridge to receive zeni tokens from users and convert them to in-game gzeni
 */
contract ZeniGameVault is ReentrancyGuard, Ownable {
    using ECDSA for bytes32;

    // Zeni token interface
    IERC20 public zeniToken;
    
    // Server signature verification address
    address public signerAddress;
    
    // Operational wallet address (for change refills)
    address public operationalWallet;
    
    // Auto-refill feature enable/disable
    bool public autoRefillEnabled = false;
    
    // Maximum auto-refill amount (limit per withdrawal request)
    uint256 public maxAutoRefillAmount = 10000 * 10**18; // 10,000 zeni
    
    // User deposit tracking
    mapping(address => uint256) public deposited;
    
    // Used nonces (prevent replay attacks)
    mapping(bytes32 => bool) public usedNonces;
    
    // Processed refills from operational wallet
    mapping(bytes32 => bool) public processedRefills;
    
    // Game-related settings
    uint256 public minDepositAmount = 1 * 10**18; // Minimum deposit amount (1 zeni)
    uint256 public withdrawalFee = 0; // Withdrawal fee (%)
    
    // Events
    event Deposit(address indexed user, uint256 amount, uint256 timestamp);
    event Withdraw(address indexed user, uint256 amount, uint256 timestamp);
    event SignerUpdated(address indexed oldSigner, address indexed newSigner);
    event MinDepositUpdated(uint256 newMinDeposit);
    event WithdrawalFeeUpdated(uint256 newFee);
    event OperationalWalletUpdated(address indexed oldWallet, address indexed newWallet);
    event OperationalDeposit(address indexed wallet, uint256 amount, uint256 timestamp);
    event AutoRefillStatusUpdated(bool enabled);
    event MaxAutoRefillAmountUpdated(uint256 newAmount);
    event AutoRefill(address indexed to, uint256 amount, bytes32 refillId);
    event ZeniTokenUpdated(address indexed oldToken, address indexed newToken);
    
    /**
     * @dev Constructor
     * @param _zeniToken Zeni token contract address
     * @param _signer Server public address used for signature verification
     */
    constructor(address _zeniToken, address _signer) Ownable(msg.sender) {
        require(_zeniToken != address(0), "Invalid zeni token address");
        require(_signer != address(0), "Invalid signer address");
        
        zeniToken = IERC20(_zeniToken);
        signerAddress = _signer;
        _transferOwnership(msg.sender);
    }
    
    /**
     * @dev Update token contract address (for Szeni test support)
     * @param _newTokenAddress New token contract address
     */
    function setZeniToken(address _newTokenAddress) external onlyOwner {
        require(_newTokenAddress != address(0), "Invalid token address");
        
        address oldToken = address(zeniToken);
        zeniToken = IERC20(_newTokenAddress);
        
        emit ZeniTokenUpdated(oldToken, _newTokenAddress);
    }
    
    /**
     * @dev Set operational wallet
     * @param _newWallet New operational wallet address
     */
    function setOperationalWallet(address _newWallet) external onlyOwner {
        require(_newWallet != address(0), "Invalid wallet address");
        
        address oldWallet = operationalWallet;
        operationalWallet = _newWallet;
        
        emit OperationalWalletUpdated(oldWallet, _newWallet);
    }
    
    /**
     * @dev Set auto-refill feature enable/disable
     * @param _enabled True to enable
     */
    function setAutoRefillEnabled(bool _enabled) external onlyOwner {
        autoRefillEnabled = _enabled;
        emit AutoRefillStatusUpdated(_enabled);
    }
    
    /**
     * @dev Set maximum auto-refill amount
     * @param _newAmount New maximum refill amount
     */
    function setMaxAutoRefillAmount(uint256 _newAmount) external onlyOwner {
        require(_newAmount > 0, "Amount must be greater than 0");
        maxAutoRefillAmount = _newAmount;
        emit MaxAutoRefillAmountUpdated(_newAmount);
    }
    
    /**
     * @dev Refill zeni from operational wallet (manual)
     * @param _amount Refill amount
     */
    function depositFromOperationalWallet(uint256 _amount) external nonReentrant {
        require(msg.sender == operationalWallet, "Not operational wallet");
        require(_amount > 0, "Amount must be greater than 0");
        
        // Transfer zeni to contract
        bool success = zeniToken.transferFrom(msg.sender, address(this), _amount);
        require(success, "Token transfer failed");
        
        emit OperationalDeposit(msg.sender, _amount, block.timestamp);
    }
    
    /**
     * @dev Deposit zeni to game
     * @param _amount Deposit amount
     */
    function depositToGame(uint256 _amount) external nonReentrant {
        require(_amount >= minDepositAmount, "Amount below minimum");
        
        // Transfer zeni to contract
        bool success = zeniToken.transferFrom(msg.sender, address(this), _amount);
        require(success, "Token transfer failed");
        
        // Update deposit record
        deposited[msg.sender] += _amount;
        
        // Event notification to game backend
        emit Deposit(msg.sender, _amount, block.timestamp);
    }
    
    /**
     * @dev Execute auto-refill from operations (with server signature)
     * @param _to Withdrawal destination address
     * @param _amount Refill amount
     * @param _refillId Unique refill ID
     * @param _signature Server signature data
     */
    function autoRefill(
        address _to,
        uint256 _amount, 
        bytes32 _refillId,
        bytes calldata _signature
    ) external nonReentrant {
        require(autoRefillEnabled, "Auto refill not enabled");
        require(_to != address(0), "Invalid recipient");
        require(_amount > 0 && _amount <= maxAutoRefillAmount, "Invalid amount");
        require(!processedRefills[_refillId], "Refill ID already used");
        
        // Create message hash
        bytes32 messageHash = keccak256(abi.encodePacked(
            _to,
            _amount,
            _refillId,
            address(this)
        ));
        
        // Verify signature
        bytes32 ethSignedMessageHash = MessageHashUtils.toEthSignedMessageHash(messageHash);
        address recoveredSigner = ECDSA.recover(ethSignedMessageHash, _signature);
        require(recoveredSigner == signerAddress, "Invalid signature");
        
        // Mark refill ID as used
        processedRefills[_refillId] = true;
        
        // Transfer zeni from operational wallet to contract
        bool success1 = zeniToken.transferFrom(operationalWallet, address(this), _amount);
        require(success1, "Refill transfer failed");
        
        // Transfer zeni from contract to user (auto-bridge functionality)
        bool success2 = zeniToken.transfer(_to, _amount);
        require(success2, "User transfer failed");
        
        emit AutoRefill(_to, _amount, _refillId);
    }
    
    /**
     * @dev Withdraw zeni from game (requires approval from game server)
     * @param _amount Withdrawal amount
     * @param _nonce Unique nonce value (prevent replay attacks)
     * @param _signature Server signature data
     */
    function withdrawFromGame(
        uint256 _amount, 
        bytes32 _nonce,
        bytes calldata _signature
    ) external nonReentrant {
        require(_amount > 0, "Amount must be greater than 0");
        require(!usedNonces[_nonce], "Nonce already used");
        
        // Create message hash
        bytes32 messageHash = keccak256(abi.encodePacked(
            msg.sender, 
            _amount, 
            _nonce, 
            address(this)
        ));
        
        // Verify signature
        bytes32 ethSignedMessageHash = MessageHashUtils.toEthSignedMessageHash(messageHash);
        address recoveredSigner = ECDSA.recover(ethSignedMessageHash, _signature);
        require(recoveredSigner == signerAddress, "Invalid signature");
        
        // Mark nonce as used
        usedNonces[_nonce] = true;
        
        // Calculate fee (if necessary)
        uint256 fee = 0;
        if (withdrawalFee > 0) {
            fee = (_amount * withdrawalFee) / 10000; // Example: 100 = 1%
        }
        
        uint256 amountAfterFee = _amount - fee;
        
        // Return zeni
        bool success = zeniToken.transfer(msg.sender, amountAfterFee);
        require(success, "Token transfer failed");
        
        emit Withdraw(msg.sender, amountAfterFee, block.timestamp);
    }
    
    /**
     * @dev Withdraw zeni for user (admin function for auto-bridge)
     * @param _user User address to receive tokens
     * @param _amount Withdrawal amount
     * @param _nonce Unique nonce value (prevent replay attacks)
     * @param _signature Server signature data
     */
    function withdrawForUser(
        address _user,
        uint256 _amount, 
        bytes32 _nonce,
        bytes calldata _signature
    ) external onlyOwner nonReentrant {
        require(_user != address(0), "Invalid user address");
        require(_amount > 0, "Amount must be greater than 0");
        require(!usedNonces[_nonce], "Nonce already used");
        
        // Create message hash (same format as withdrawFromGame for consistency)
        bytes32 messageHash = keccak256(abi.encodePacked(
            _user, 
            _amount, 
            _nonce, 
            address(this)
        ));
        
        // Verify signature
        bytes32 ethSignedMessageHash = MessageHashUtils.toEthSignedMessageHash(messageHash);
        address recoveredSigner = ECDSA.recover(ethSignedMessageHash, _signature);
        require(recoveredSigner == signerAddress, "Invalid signature");
        
        // Mark nonce as used
        usedNonces[_nonce] = true;
        
        // Calculate fee (if necessary)
        uint256 fee = 0;
        if (withdrawalFee > 0) {
            fee = (_amount * withdrawalFee) / 10000; // Example: 100 = 1%
        }
        
        uint256 amountAfterFee = _amount - fee;
        
        // Transfer zeni to user
        bool success = zeniToken.transfer(_user, amountAfterFee);
        require(success, "Token transfer failed");
        
        emit Withdraw(_user, amountAfterFee, block.timestamp);
    }
    
    /**
     * @dev Update signer address (for emergencies)
     * @param _newSigner New signer address
     */
    function updateSigner(address _newSigner) external onlyOwner {
        require(_newSigner != address(0), "Invalid address");
        
        address oldSigner = signerAddress;
        signerAddress = _newSigner;
        
        emit SignerUpdated(oldSigner, _newSigner);
    }
    
    /**
     * @dev Update minimum deposit amount
     * @param _newMinDeposit New minimum deposit amount
     */
    function updateMinDepositAmount(uint256 _newMinDeposit) external onlyOwner {
        minDepositAmount = _newMinDeposit;
        emit MinDepositUpdated(_newMinDeposit);
    }
    
    /**
     * @dev Update withdrawal fee
     * @param _newFee New fee (basis points, e.g.: 100 = 1%)
     */
    function updateWithdrawalFee(uint256 _newFee) external onlyOwner {
        require(_newFee <= 1000, "Fee too high"); // Maximum 10%
        withdrawalFee = _newFee;
        emit WithdrawalFeeUpdated(_newFee);
    }
    
    /**
     * @dev Check user deposit balance
     * @param _user User address to check
     * @return Deposit balance
     */
    function getDeposited(address _user) external view returns (uint256) {
        return deposited[_user];
    }
    
    /**
     * @dev Get total deposited amount
     * @return Total deposited amount
     */
    function getTotalDeposited() external view returns (uint256) {
        return zeniToken.balanceOf(address(this));
    }
    
    /**
     * @dev Check if nonce is used
     * @param _nonce Nonce to check
     * @return True if used
     */
    function isNonceUsed(bytes32 _nonce) external view returns (bool) {
        return usedNonces[_nonce];
    }
    
    /**
     * @dev Check if refill ID is used
     * @param _refillId Refill ID to check
     * @return True if used
     */
    function isRefillProcessed(bytes32 _refillId) external view returns (bool) {
        return processedRefills[_refillId];
    }
    
    /**
     * @dev Emergency withdrawal of all zeni tokens from contract
     * @param _to Recipient address
     */
    function emergencyWithdraw(address _to) external onlyOwner {
        require(_to != address(0), "Invalid address");
        
        uint256 balance = zeniToken.balanceOf(address(this));
        require(balance > 0, "No tokens to withdraw");
        
        bool success = zeniToken.transfer(_to, balance);
        require(success, "Token transfer failed");
    }
} 
"
    },
    "utils/MessageHashUtils.sol": {
      "content": "// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

library MessageHashUtils {
    function toEthSignedMessageHash(bytes32 hash) internal pure returns (bytes32) {
        return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\
32", hash));
    }
}
"
    }
  },
  "settings": {
    "optimizer": {
      "enabled": true,
      "runs": 200
    },
    "viaIR": true,
    "evmVersion": "paris",
    "outputSelection": {
      "*": {
        "*": [
          "evm.bytecode",
          "evm.deployedBytecode",
          "devdoc",
          "userdoc",
          "metadata",
          "abi"
        ]
      }
    }
  }
}}

Tags:
ERC20, Multisig, Multi-Signature, Factory|addr:0x3474d16fb1f75ec1dc3ee130420e999d57d20a5f|verified:true|block:23490689|tx:0x478f7c0e63714a8a4b064fd9b60bd47b7999d577e4b5bcd336e6dc78c5f6b80b|first_check:1759418938

Submitted on: 2025-10-02 17:28:59

Comments

Log in to comment.

No comments yet.