GameDataStorage

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

import "@openzeppelin/contracts/access/Ownable.sol";

/**
 * @title GameDataStorage
 * @notice Stores game-specific data with automatic chunking (like PromptedImageStoreRawBytes)
 * @dev Handles chunking internally - just pass arrays of game data
 */
contract GameDataStorage is Ownable {
    
    /*//////////////////////////////////////////////////////////////
                                 ERRORS
    //////////////////////////////////////////////////////////////*/
    
    error InvalidTokenId();
    error GameDataAlreadySet();
    error GameDataNotSet();
    error InvalidGameData();
    error ArrayLengthMismatch();
    
    /*//////////////////////////////////////////////////////////////
                                 EVENTS
    //////////////////////////////////////////////////////////////*/
    
    event GameDataStored(uint256 indexed fromTokenId, uint256 indexed toTokenId);
    event GameDataUpdated(uint256[] tokenIds);
    
    /*//////////////////////////////////////////////////////////////
                            STATE VARIABLES
    //////////////////////////////////////////////////////////////*/
    
    uint256 constant MAX_BYTES = 24575;
    
    struct DataLocation {
        address pointer;
        uint48 start;
        uint48 end;
    }
    
    // tokenId => data location
    mapping(uint256 => DataLocation) public idToLocation;
    
    // Track which tokens have data
    mapping(uint256 => bool) public hasGameData;
    
    // Maximum token ID (set during construction)
    uint256 public immutable MAX_TOKEN_ID;
    
    /*//////////////////////////////////////////////////////////////
                              CONSTRUCTOR
    //////////////////////////////////////////////////////////////*/
    
    constructor(uint256 maxTokenId) Ownable(msg.sender) {
        require(maxTokenId > 0, "Max token ID must be greater than 0");
        MAX_TOKEN_ID = maxTokenId;
    }
    
    /*//////////////////////////////////////////////////////////////
                           STORAGE FUNCTIONS
    //////////////////////////////////////////////////////////////*/
    
    /**
     * @notice Store game data for multiple tokens with automatic chunking
     * @dev Handles chunking internally like PromptedImageStoreRawBytes
     * @param tokenIds Array of token IDs (must be sequential or specify which tokens)
     * @param gameDatas Array of game data corresponding to each token
     */
    function save(
        uint256[] calldata tokenIds,
        bytes[] calldata gameDatas
    ) external onlyOwner {
        if (tokenIds.length != gameDatas.length) revert ArrayLengthMismatch();
        require(tokenIds.length > 0, "Empty arrays");
        
        uint256 dataLengthCounter;
        bytes memory buffer;
        bytes memory queue;
        uint256 queueLength;
        uint256 tokenIdIndex = 0;
        
        for (uint256 i; i < gameDatas.length; ++i) {
            // Validate token ID
            uint256 tokenId = tokenIds[i];
            if (tokenId == 0 || tokenId > MAX_TOKEN_ID) revert InvalidTokenId();
            if (hasGameData[tokenId]) revert GameDataAlreadySet();
            
            uint256 possibleEnd = dataLengthCounter + gameDatas[i].length;
            
            if (possibleEnd <= MAX_BYTES) {
                queue = bytes.concat(
                    queue,
                    abi.encode(DataLocation(address(0), uint48(dataLengthCounter), uint48(possibleEnd)))
                );
                dataLengthCounter = possibleEnd;
                queueLength++;
                buffer = bytes.concat(buffer, gameDatas[i]);
            }
            
            if (possibleEnd > MAX_BYTES || i == gameDatas.length - 1) {
                address pointer = SSTORE2Write(buffer);
                DataLocation[] memory decodedQueue = abi.decode(
                    bytes.concat(abi.encode(uint256(32), queueLength), queue),
                    (DataLocation[])
                );
                
                for (uint j; j < decodedQueue.length; j++) {
                    decodedQueue[j].pointer = pointer;
                    uint256 currentTokenId = tokenIds[tokenIdIndex];
                    idToLocation[currentTokenId] = decodedQueue[j];
                    hasGameData[currentTokenId] = true;
                    tokenIdIndex++;
                }
                
                dataLengthCounter = 0;
                queue = "";
                queueLength = 0;
                buffer = "";
                
                if (i != gameDatas.length - 1) {
                    i--;
                    continue;
                }
                
                if (possibleEnd > MAX_BYTES) {
                    pointer = SSTORE2Write(gameDatas[i]);
                    uint256 currentTokenId = tokenIds[tokenIdIndex];
                    idToLocation[currentTokenId] = DataLocation(pointer, 0, uint48(gameDatas[i].length));
                    hasGameData[currentTokenId] = true;
                    tokenIdIndex++;
                }
            }
        }
        
        emit GameDataStored(tokenIds[0], tokenIds[tokenIds.length - 1]);
    }
    
    /**
     * @notice Overwrite existing game data for specified tokens
     * @param tokenIds Array of token IDs to update
     * @param gameDatas Array of new game data
     */
    function overwrite(
        uint256[] calldata tokenIds,
        bytes[] calldata gameDatas
    ) external onlyOwner {
        if (tokenIds.length != gameDatas.length) revert ArrayLengthMismatch();
        require(tokenIds.length > 0, "Empty arrays");
        
        uint256 dataLengthCounter;
        bytes memory buffer;
        bytes memory queue;
        uint256 queueLength;
        uint256 tokenIdIndex = 0;
        
        for (uint256 i; i < gameDatas.length; ++i) {
            // Validate token ID
            uint256 tokenId = tokenIds[i];
            if (tokenId == 0 || tokenId > MAX_TOKEN_ID) revert InvalidTokenId();
            
            uint256 possibleEnd = dataLengthCounter + gameDatas[i].length;
            
            if (possibleEnd <= MAX_BYTES) {
                queue = bytes.concat(
                    queue,
                    abi.encode(DataLocation(address(0), uint48(dataLengthCounter), uint48(possibleEnd)))
                );
                dataLengthCounter = possibleEnd;
                queueLength++;
                buffer = bytes.concat(buffer, gameDatas[i]);
            }
            
            if (possibleEnd > MAX_BYTES || i == gameDatas.length - 1) {
                address pointer = SSTORE2Write(buffer);
                DataLocation[] memory decodedQueue = abi.decode(
                    bytes.concat(abi.encode(uint256(32), queueLength), queue),
                    (DataLocation[])
                );
                
                for (uint j; j < decodedQueue.length; j++) {
                    decodedQueue[j].pointer = pointer;
                    uint256 currentTokenId = tokenIds[tokenIdIndex];
                    idToLocation[currentTokenId] = decodedQueue[j];
                    hasGameData[currentTokenId] = true;
                    tokenIdIndex++;
                }
                
                dataLengthCounter = 0;
                queue = "";
                queueLength = 0;
                buffer = "";
                
                if (i != gameDatas.length - 1) {
                    i--;
                    continue;
                }
                
                if (possibleEnd > MAX_BYTES) {
                    pointer = SSTORE2Write(gameDatas[i]);
                    uint256 currentTokenId = tokenIds[tokenIdIndex];
                    idToLocation[currentTokenId] = DataLocation(pointer, 0, uint48(gameDatas[i].length));
                    hasGameData[currentTokenId] = true;
                }
            }
        }
        
        emit GameDataUpdated(tokenIds);
    }
    
    /*//////////////////////////////////////////////////////////////
                            VIEW FUNCTIONS
    //////////////////////////////////////////////////////////////*/
    
    /**
     * @notice Get game data for a token as a string
     * @param tokenId The token ID
     * @return Game data as a JavaScript string
     */
    function getGameData(uint256 tokenId) external view returns (string memory) {
        if (tokenId == 0 || tokenId > MAX_TOKEN_ID) revert InvalidTokenId();
        
        DataLocation memory loc = idToLocation[tokenId];
        if (loc.pointer == address(0)) {
            return "";
        }
        
        return string(SSTORE2Read(loc.pointer, loc.start, loc.end));
    }
    
    /**
     * @notice Check if a token has game data
     * @param tokenId The token ID
     */
    function tokenHasGameData(uint256 tokenId) external view returns (bool) {
        return hasGameData[tokenId];
    }
    
    /*//////////////////////////////////////////////////////////////
                         INTERNAL SSTORE2 LOGIC
    //////////////////////////////////////////////////////////////*/
    
    function SSTORE2Write(bytes memory data) internal returns (address pointer) {
        bytes memory runtimeCode = abi.encodePacked(hex"00", data);
        bytes memory creationCode = abi.encodePacked(
            hex"63",
            uint32(runtimeCode.length),
            hex"80_60_0E_60_00_39_60_00_F3",
            runtimeCode
        );
        
        assembly {
            pointer := create(0, add(creationCode, 0x20), mload(creationCode))
            if iszero(pointer) {
                revert(0, 0)
            }
        }
    }
    
    function SSTORE2Read(address pointer, uint256 start, uint256 end) internal view returns (bytes memory) {
        bytes memory data = new bytes(end - start);
        
        assembly {
            extcodecopy(pointer, add(data, 0x20), add(start, 1), sub(end, start))
        }
        
        return data;
    }
}
"
    },
    "@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:
Multisig, Multi-Signature, Factory|addr:0x1dfb5daea27ae29c085e04641a1b85108caef105|verified:true|block:23732803|tx:0x88ba09882b8fd9175dfc9c4a7a28226b246feec7cc00e5e75cc48a496245476d|first_check:1762349550

Submitted on: 2025-11-05 14:32:32

Comments

Log in to comment.

No comments yet.