// SPDX-License-Identifier: MIT pragma solidity ^0.8.24; interface IERC20BoardV5 { function transfer(address to, uint256 value) external returns (bool); function transferFrom(address from, address to, uint256 value) external returns (bool); function balanceOf(address account) external view returns (uint256); } contract OwnTheGridBoardV5 { uint16 public constant SLOT_COUNT = 400; uint16 public constant MAX_BATCH = 40; uint256 public constant SLOT_LOCK = 250_000 ether; uint256 public constant MAX_BOARD_LOCK = 100_000_000 ether; uint64 public constant DEFAULT_TERM = 30 days; struct TileData { string name; string url; string message; uint24 backgroundColor; uint24 accentColor; uint24 textColor; } struct Slot { address owner; uint72 colors; bool hidden; uint16 priceRank; uint128 lockedAmount; uint64 expiresAt; uint64 reservedAtBlock; uint64 updatedAtBlock; string name; string url; string message; } IERC20BoardV5 public immutable token; address public owner; address public moderator; uint16 public occupiedCount; uint256 public totalLocked; mapping(uint16 => Slot) private slotRecords; mapping(address => uint256) public pendingWithdrawals; bool private entered; event SlotReserved(uint16 indexed slotId, address indexed account, uint256 lockedAmount, uint64 expiresAt); event SlotBatchReserved(address indexed account, uint16 count, uint256 totalLockedAmount); event SlotMetadataUpdated(uint16 indexed slotId, address indexed account); event SlotReleased(uint16 indexed slotId, address indexed account, uint256 unlockedAmount); event SlotBatchReleased(address indexed account, uint16 count, uint256 unlockedAmount); event SlotHidden(uint16 indexed slotId, bool hidden, bytes32 reasonHash); event ModeratorUpdated(address indexed moderator); event OwnershipTransferred(address indexed previousOwner, address indexed nextOwner); modifier onlyOwner() { require(msg.sender == owner, "not owner"); _; } modifier nonReentrant() { require(!entered, "reentrant"); entered = true; _; entered = false; } constructor(address acceptedToken, address initialModerator) { require(acceptedToken.code.length > 0, "token required"); token = IERC20BoardV5(acceptedToken); owner = msg.sender; moderator = initialModerator == address(0) ? msg.sender : initialModerator; emit OwnershipTransferred(address(0), msg.sender); emit ModeratorUpdated(moderator); } function version() external pure returns (uint256) { return 5; } function getSlot(uint16 slotId) external view returns (Slot memory) { require(slotId < SLOT_COUNT, "bad slot"); return slotRecords[slotId]; } function getSlots(uint16 start, uint16 count) external view returns (Slot[] memory result) { require(count > 0 && count <= MAX_BATCH && uint256(start) + count <= SLOT_COUNT, "bad page"); result = new Slot[](count); for (uint16 i = 0; i < count; i++) result[i] = slotRecords[start + i]; } // Kept for backwards-compatible integrations; every valid slot has the same price in V5. function requiredLockForRank(uint256 rank) public pure returns (uint256) { require(rank > 0 && rank <= SLOT_COUNT, "bad rank"); return SLOT_LOCK; } function nextRequiredLock() external view returns (uint256) { if (occupiedCount < SLOT_COUNT) return SLOT_LOCK; for (uint16 slotId = 0; slotId < SLOT_COUNT; slotId++) { if (block.timestamp >= slotRecords[slotId].expiresAt) return SLOT_LOCK; } return 0; } function isAvailable(uint16 slotId) public view returns (bool) { require(slotId < SLOT_COUNT, "bad slot"); return slotRecords[slotId].owner == address(0) || block.timestamp >= slotRecords[slotId].expiresAt; } function quoteReserve(uint16[] calldata slotIds) external view returns (uint256[] memory amounts, uint256 totalAmount) { _validateSlotIds(slotIds); amounts = new uint256[](slotIds.length); for (uint256 i = 0; i < slotIds.length; i++) { require(isAvailable(slotIds[i]), "slot taken"); amounts[i] = SLOT_LOCK; } totalAmount = SLOT_LOCK * slotIds.length; } function reserve(uint16 slotId, TileData calldata data, uint256 maxAmount) external nonReentrant { require(slotId < SLOT_COUNT, "bad slot"); _validateData(data); _clearExpired(slotId); require(slotRecords[slotId].owner == address(0), "slot taken"); require(SLOT_LOCK <= maxAmount, "price changed"); _checkCap(SLOT_LOCK); _collect(SLOT_LOCK); _writeSlot(slotId, data); occupiedCount++; totalLocked += SLOT_LOCK; } function reserveBatch(uint16[] calldata slotIds, TileData[] calldata data, uint256 maxAmount) external nonReentrant { _validateSlotIds(slotIds); require(data.length == slotIds.length, "length mismatch"); for (uint256 i = 0; i < slotIds.length; i++) { _validateData(data[i]); _clearExpired(slotIds[i]); require(slotRecords[slotIds[i]].owner == address(0), "slot taken"); } uint256 totalAmount = SLOT_LOCK * slotIds.length; require(totalAmount <= maxAmount, "price changed"); _checkCap(totalAmount); _collect(totalAmount); for (uint256 i = 0; i < slotIds.length; i++) _writeSlot(slotIds[i], data[i]); occupiedCount += uint16(slotIds.length); totalLocked += totalAmount; emit SlotBatchReserved(msg.sender, uint16(slotIds.length), totalAmount); } function updateMetadata(uint16 slotId, TileData calldata data) external { require(slotId < SLOT_COUNT, "bad slot"); _validateData(data); Slot storage slot = slotRecords[slotId]; require(slot.owner == msg.sender, "not slot owner"); require(block.timestamp < slot.expiresAt, "expired"); _writeData(slot, data); slot.updatedAtBlock = _chainBlockNumber(); emit SlotMetadataUpdated(slotId, msg.sender); } function release(uint16 slotId) external nonReentrant { require(slotId < SLOT_COUNT, "bad slot"); Slot memory slot = slotRecords[slotId]; require(slot.owner == msg.sender, "not slot owner"); _deleteSlot(slotId, slot.owner, slot.lockedAmount); require(token.transfer(slot.owner, slot.lockedAmount), "transfer failed"); } function releaseBatch(uint16[] calldata slotIds) external nonReentrant { _validateSlotIds(slotIds); uint256 amount; for (uint256 i = 0; i < slotIds.length; i++) { Slot storage slot = slotRecords[slotIds[i]]; require(slot.owner == msg.sender, "not slot owner"); uint256 deposit = slot.lockedAmount; amount += deposit; _deleteSlot(slotIds[i], msg.sender, deposit); } require(token.transfer(msg.sender, amount), "transfer failed"); emit SlotBatchReleased(msg.sender, uint16(slotIds.length), amount); } function withdraw() external nonReentrant { uint256 amount = pendingWithdrawals[msg.sender]; require(amount > 0, "nothing to withdraw"); pendingWithdrawals[msg.sender] = 0; require(token.transfer(msg.sender, amount), "transfer failed"); } function clearExpiredBatch(uint16[] calldata slotIds) external nonReentrant { _validateSlotIds(slotIds); for (uint256 i = 0; i < slotIds.length; i++) _clearExpired(slotIds[i]); } function hideSlot(uint16 slotId, bool hidden, bytes32 reasonHash) external { require(msg.sender == owner || msg.sender == moderator, "not moderator"); require(slotId < SLOT_COUNT && slotRecords[slotId].owner != address(0), "empty slot"); slotRecords[slotId].hidden = hidden; emit SlotHidden(slotId, hidden, reasonHash); } function setModerator(address nextModerator) external onlyOwner { moderator = nextModerator == address(0) ? owner : nextModerator; emit ModeratorUpdated(moderator); } function transferOwnership(address nextOwner) external onlyOwner { require(nextOwner != address(0), "owner required"); emit OwnershipTransferred(owner, nextOwner); owner = nextOwner; } function _validateSlotIds(uint16[] calldata slotIds) private pure { require(slotIds.length > 0 && slotIds.length <= MAX_BATCH, "bad batch"); for (uint256 i = 0; i < slotIds.length; i++) { require(slotIds[i] < SLOT_COUNT, "bad slot"); for (uint256 j = 0; j < i; j++) require(slotIds[j] != slotIds[i], "duplicate slot"); } } function _validateData(TileData calldata data) private pure { bytes calldata name = bytes(data.name); require(name.length > 0 && name.length <= 96, "bad name"); bool visible; for (uint256 i = 0; i < name.length; i++) if (uint8(name[i]) > 32) visible = true; require(visible, "bad name"); bytes calldata url = bytes(data.url); require(url.length > 0 && url.length <= 256, "bad url"); bool http = url.length >= 7 && url[0] == "h" && url[1] == "t" && url[2] == "t" && url[3] == "p" && ((url[4] == ":" && url[5] == "/" && url[6] == "/") || (url.length >= 8 && url[4] == "s" && url[5] == ":" && url[6] == "/" && url[7] == "/")); require(http, "bad url"); require(bytes(data.message).length <= 336, "message too long"); } function _checkCap(uint256 amount) private view { require(totalLocked + amount <= MAX_BOARD_LOCK, "board cap"); } function _chainBlockNumber() private view returns (uint64) { (bool ok, bytes memory result) = address(0x64).staticcall{gas: 10000}(abi.encodeWithSignature("arbBlockNumber()")); return uint64(ok && result.length == 32 ? abi.decode(result, (uint256)) : block.number); } function _collect(uint256 amount) private { uint256 beforeBalance = token.balanceOf(address(this)); require(token.transferFrom(msg.sender, address(this), amount), "transfer failed"); require(token.balanceOf(address(this)) == beforeBalance + amount, "unsupported transfer fee"); } function _writeSlot(uint16 slotId, TileData calldata data) private { Slot storage slot = slotRecords[slotId]; slot.owner = msg.sender; slot.hidden = false; slot.priceRank = 0; slot.lockedAmount = uint128(SLOT_LOCK); slot.expiresAt = uint64(block.timestamp + DEFAULT_TERM); slot.reservedAtBlock = _chainBlockNumber(); slot.updatedAtBlock = _chainBlockNumber(); _writeData(slot, data); emit SlotReserved(slotId, msg.sender, SLOT_LOCK, slot.expiresAt); } function _writeData(Slot storage slot, TileData calldata data) private { slot.name = data.name; slot.url = data.url; slot.message = data.message; slot.colors = uint72(data.backgroundColor) << 48 | uint72(data.accentColor) << 24 | uint72(data.textColor); } function _clearExpired(uint16 slotId) private { Slot storage slot = slotRecords[slotId]; if (slot.owner != address(0) && block.timestamp >= slot.expiresAt) { address account = slot.owner; uint256 amount = slot.lockedAmount; pendingWithdrawals[account] += amount; _deleteSlot(slotId, account, amount); } } function _deleteSlot(uint16 slotId, address account, uint256 amount) private { delete slotRecords[slotId]; occupiedCount--; totalLocked -= amount; emit SlotReleased(slotId, account, amount); } }