a manifold extension to port fully onchain an existing collection minted with offchain content
clgsms/contracts/ColorgasmStorage.sol
Solidity · 124 lines · 5 KBRAWDOWNLOAD
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {SSTORE2} from "solady/src/utils/SSTORE2.sol";
/// @title ColorgasmStorage — chunked onchain blob storage
///
/// @notice Holds arbitrary bytes as SSTORE2 chunks under a caller-chosen key,
/// written one chunk per transaction. The key is the pointer: once a
/// blob is sealed, `chunksOf(key)` is all a renderer needs.
///
/// Deliberately generic and permissionless. The first address to write
/// a key owns it, so nobody can overwrite someone else's blob, and
/// sealing is irreversible — a sealed key's bytes can never change,
/// which is the property a collector actually wants to verify.
///
/// @dev SSTORE2 code deposit is the cheapest persistent storage the EVM
/// offers: 200 gas per byte, against 625 gas per byte for regular
/// storage slots. Measured all-in with calldata and transaction
/// overhead it comes to ~221 gas per byte.
///
/// Chunk size: EIP-7825 caps a transaction at 16,777,216 gas and a
/// max-size chunk costs ~5.53M, so one chunk per transaction is the
/// comfortable shape (and the reason EthFS and ScriptyStorageV2 work
/// the same way). MAX_CHUNK is the SSTORE2 limit rounded down to a
/// multiple of 3 so that base64 encoding can be done chunk by chunk at
/// render time — see ColorgasmRendererV3.
contract ColorgasmStorage {
error ChunkTooLarge();
error EmptyChunk();
error KeyTaken();
error KeySealed();
error NotOwner();
error NothingStored();
event ChunkAppended(bytes32 indexed key, uint256 index, uint256 size, address pointer);
event KeyFinalised(bytes32 indexed key, uint256 chunks, uint256 size);
/// @notice Largest chunk accepted: SSTORE2's 24575-byte limit rounded down
/// to a multiple of 3 (3 x 8191), so base64 encodes chunk-locally.
uint256 public constant MAX_CHUNK = 24573;
/// @notice Whoever wrote a key's first chunk. Only they can extend or seal it.
mapping(bytes32 => address) public ownerOf;
/// @notice True once a key can never be written again.
mapping(bytes32 => bool) public isSealed;
mapping(bytes32 => address[]) private _chunks;
mapping(bytes32 => uint256) private _size;
/// @notice Append one chunk to a key. One transaction per chunk.
/// @return index position of this chunk in the blob.
function append(bytes32 key, bytes calldata chunk) external returns (uint256 index) {
if (chunk.length == 0) revert EmptyChunk();
if (chunk.length > MAX_CHUNK) revert ChunkTooLarge();
if (isSealed[key]) revert KeySealed();
address owner = ownerOf[key];
if (owner == address(0)) ownerOf[key] = msg.sender;
else if (owner != msg.sender) revert NotOwner();
address pointer = SSTORE2.write(chunk);
index = _chunks[key].length;
_chunks[key].push(pointer);
unchecked {
_size[key] += chunk.length;
}
emit ChunkAppended(key, index, chunk.length, pointer);
}
/// @notice Freeze a key forever. Irreversible, and the point of the whole
/// exercise — after this the bytes behind the pointer cannot change.
function seal(bytes32 key) external {
if (ownerOf[key] != msg.sender) revert NotOwner();
if (isSealed[key]) revert KeySealed();
if (_chunks[key].length == 0) revert NothingStored();
isSealed[key] = true;
emit KeyFinalised(key, _chunks[key].length, _size[key]);
}
/// @notice The SSTORE2 pointers holding a blob, in order. This is what a
/// renderer reads — it never needs the bytes through this contract.
function chunksOf(bytes32 key) external view returns (address[] memory) {
return _chunks[key];
}
/// @notice Total bytes stored under a key.
function sizeOf(bytes32 key) external view returns (uint256) {
return _size[key];
}
/// @notice Number of chunks under a key.
function chunkCount(bytes32 key) external view returns (uint256) {
return _chunks[key].length;
}
/// @notice The whole blob, assembled.
/// @dev One allocation, one `extcodecopy` per pointer straight into
/// place — linear, where repeated `bytes.concat` is quadratic.
/// Convenience only: a renderer assembling a document should read
/// `chunksOf` and copy into its own output buffer instead, to avoid
/// paying for this intermediate copy at all.
function read(bytes32 key) external view returns (bytes memory out) {
address[] memory pointers = _chunks[key];
uint256 n = pointers.length;
if (n == 0) return out;
uint256 total = _size[key];
assembly {
out := mload(0x40)
mstore(out, total)
mstore(0x40, add(out, and(add(add(total, 0x20), 0x1f), not(0x1f))))
let dst := add(out, 0x20)
for { let i := 0 } lt(i, n) { i := add(i, 1) } {
let ptr := mload(add(pointers, add(0x20, mul(i, 0x20))))
let size := sub(extcodesize(ptr), 1)
extcodecopy(ptr, dst, 1, size)
dst := add(dst, size)
}
}
}
}