HoodTalkRegistry
0x0e0c35e444e2a24374f46fd2792baf5e81c31d41
Verification
Verified
v0.8.24+commit.e11b9ed9
Type
Contract
4,726 bytes
ABI entries
34
15 read · 3 write
License
none
Contract information
- Address
- 0x0e0c35e444e2a24374f46fd2792baf5e81c31d41
- Chain
- Robinhood Chain (4663)
- Compiler
- v0.8.24+commit.e11b9ed9
- Optimization
- Enabled
- Creator
- 0xC7c165bA3f…b1DC803941
- Creation tx
- 0xa7b8718f4c…8e767258e9
Token
Not a token
This contract does not expose ERC-20 metadata.
Read contract (15)
HOOD_TALK_TYPEHASH() → bytes32
MAX_QUOTE_BYTES() → uint256
UPDATE_COOLDOWN() → uint256
authorizationDigest(uint256, address, string, uint32, uint256) → bytes32
authorizedSigner() → address
currentQuote(uint256) → string
eip712Domain() → bytes1, string, string, uint256, address, bytes32, uint256[]
getHoodTalk(uint256) → string, address, uint64, uint32
hoodTalkCount(uint256) → uint32
hoodies() → address
nextUpdateAt(uint256) → uint256
owner() → address
supportsInterface(bytes4) → bool
totalHoodTalks() → uint256
uniqueHoodiesSpoken() → uint256
Events (5)
AuthorizedSignerUpdatedEIP712DomainChangedHoodTalkUpdatedOwnershipTransferredRegistryInitialized
ABI
[
{
"inputs": [
{
"internalType": "address",
"name": "hoodies_",
"type": "address"
},
{
"internalType": "address",
"name": "authorizedSigner_",
"type": "address"
},
{
"internalType": "address",
"name": "owner_",
"type": "address"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"inputs": [],
"name": "AuthorizationExpired",
"type": "error"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "availableAt",
"type": "uint256"
}
],
"name": "CooldownActive",
"type": "error"
},
{
"inputs": [],
"name": "CountOverflow",
"type": "error"
},
{
"inputs": [],
"name": "InvalidAuthorization",
"type": "error"
},
{
"inputs": [],
"name": "InvalidQuoteLength",
"type": "error"
},
{
"inputs": [],
"name": "InvalidShortString",
"type": "error"
},
{
"inputs": [],
"name": "NotOwner",
"type": "error"
},
{
"inputs": [],
"name": "NotTokenOwner",
"type": "error"
},
{
"inputs": [
{
"internalType": "string",
"name": "str",
"type": "string"
}
],
"name": "StringTooLong",
"type": "error"
},
{
"inputs": [],
"name": "ZeroAddress",
"type": "error"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "previousSigner",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "newSigner",
"type": "address"
}
],
"name": "AuthorizedSignerUpdated",
"type": "event"
},
{
"anonymous": false,
"inputs": [],
"name": "EIP712DomainChanged",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
},
{
"indexed": true,
"internalType": "address",
"name": "holder",
"type": "address"
},
{
"indexed": true,
"internalType": "uint32",
"name": "count",
"type": "uint32"
},
{
"indexed": false,
"internalType": "uint64",
"name": "updatedAt",
"type": "uint64"
},
{
"indexed": false,
"internalType": "string",
"name": "quote",
"type": "string"
}
],
"name": "HoodTalkUpdated",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "previousOwner",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "OwnershipTransferred",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "hoodiesContract",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "signer",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "registryOwner",
"type": "address"
}
],
"name": "RegistryInitialized",
"type": "event"
},
{
"inputs": [],
"name": "HOOD_TALK_TYPEHASH",
"outputs": [
{
"internalType": "bytes32",
"name": "",
"type": "bytes32"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "MAX_QUOTE_BYTES",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "UPDATE_COOLDOWN",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
},
{
"internalType": "address",
"name": "holder",
"type": "address"
},
{
"internalType": "string",
"name": "quote",
"type": "string"
},
{
"internalType": "uint32",
"name": "count",
"type": "uint32"
},
{
"internalType": "uint256",
"name": "deadline",
"type": "uint256"
}
],
"name": "authorizationDigest",
"outputs": [
{
"internalType": "bytes32",
"name": "",
"type": "bytes32"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "authorizedSigner",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "currentQuote",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "eip712Domain",
"outputs": [
{
"internalType": "bytes1",
"name": "fields",
"type": "bytes1"
},
{
"internalType": "string",
"name": "name",
"type": "string"
},
{
"internalType": "string",
"name": "version",
"type": "string"
},
{
"internalType": "uint256",
"name": "chainId",
"type": "uint256"
},
{
"internalType": "address",
"name": "verifyingContract",
"type": "address"
},
{
"internalType": "bytes32",
"name": "salt",
"type": "bytes32"
},
{
"internalType": "uint256[]",
"name": "extensions",
"type": "uint256[]"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "getHoodTalk",
"outputs": [
{
"internalType": "string",
"name": "quote",
"type": "string"
},
{
"internalType": "address",
"name": "author",
"type": "address"
},
{
"internalType": "uint64",
"name": "updatedAt",
"type": "uint64"
},
{
"internalType": "uint32",
"name": "count",
"type": "uint32"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "hoodTalkCount",
"outputs": [
{
"internalType": "uint32",
"name": "",
"type": "uint32"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "hoodies",
"outputs": [
{
"internalType": "contract IERC721",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "nextUpdateAt",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "owner",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "newSigner",
"type": "address"
}
],
"name": "setAuthorizedSigner",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
},
{
"internalType": "string",
"name": "quote",
"type": "string"
},
{
"internalType": "uint256",
"name": "deadline",
"type": "uint256"
},
{
"internalType": "bytes",
"name": "signature",
"type": "bytes"
}
],
"name": "setHoodTalk",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes4",
"name": "interfaceId",
"type": "bytes4"
}
],
"name": "supportsInterface",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "totalHoodTalks",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "transferOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "uniqueHoodiesSpoken",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
}
]Source code
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
import "@openzeppelin/contracts/utils/introspection/ERC165.sol";
import "./interfaces/IHoodTalkRegistry.sol";
/// @title OnChainHoodies Hood Talk Registry
/// @author OnChainHoodies
/// @notice Stores the latest holder-selected Hood Talk for each
/// OnChainHoodies token.
/// @dev Each token stores only its latest Hood Talk in contract state.
/// The complete historical record remains permanently available through
/// HoodTalkUpdated events.
///
/// A Hood Talk can only be committed when:
/// - The caller currently owns the referenced NFT.
/// - The authorized backend signer approved the exact quote.
/// - The authorization has not expired.
/// - The token has not recorded another Hood Talk in the past 24 hours.
///
/// The cooldown follows the Hoodie token itself and is not reset when the
/// token is transferred to another wallet.
contract HoodTalkRegistry is EIP712, ERC165, IHoodTalkRegistry {
using ECDSA for bytes32;
/// @notice The stored state for a token's latest Hood Talk.
struct HoodTalk {
string quote;
address author;
uint64 updatedAt;
uint32 count;
}
/// @notice Maximum permitted length of a Hood Talk in UTF-8 bytes.
uint256 public constant MAX_QUOTE_BYTES = 1024;
/// @notice Minimum delay between Hood Talks for the same token.
uint256 public constant UPDATE_COOLDOWN = 1 days;
/// @notice EIP-712 type hash used to authorize a Hood Talk.
bytes32 public constant HOOD_TALK_TYPEHASH = keccak256(
"HoodTalk(uint256 tokenId,address holder,bytes32 quoteHash,uint32 count,uint256 deadline)"
);
/// @notice The OnChainHoodies ERC-721 collection.
IERC721 public immutable hoodies;
/// @notice Address authorized to administer this registry.
address public owner;
/// @notice Address authorized to approve Hood Talk submissions.
address public authorizedSigner;
/// @notice Total number of Hood Talks ever committed.
uint256 public totalHoodTalks;
/// @notice Number of unique Hoodie tokens that have spoken at least once.
uint256 public uniqueHoodiesSpoken;
mapping(uint256 => HoodTalk) private _hoodTalks;
/// @notice Thrown when a caller is not the registry owner.
error NotOwner();
/// @notice Thrown when a required address is the zero address.
error ZeroAddress();
/// @notice Thrown when the caller does not currently own the token.
error NotTokenOwner();
/// @notice Thrown when the quote is empty or exceeds MAX_QUOTE_BYTES.
error InvalidQuoteLength();
/// @notice Thrown when the token attempts to update too early.
/// @param availableAt Timestamp when the token may speak again.
error CooldownActive(uint256 availableAt);
/// @notice Thrown when the backend authorization has expired.
error AuthorizationExpired();
/// @notice Thrown when the supplied signature is not valid.
error InvalidAuthorization();
/// @notice Thrown if the token's Hood Talk count reaches uint32 maximum.
error CountOverflow();
/// @notice Emitted when registry ownership changes.
/// @param previousOwner The previous registry owner.
/// @param newOwner The new registry owner.
event OwnershipTransferred(
address indexed previousOwner,
address indexed newOwner
);
/// @notice Emitted when the authorized Hood Talk signer changes.
/// @param previousSigner The previous signer.
/// @param newSigner The new signer.
event AuthorizedSignerUpdated(
address indexed previousSigner,
address indexed newSigner
);
/// @notice Emitted once when the registry is deployed.
/// @param hoodiesContract The associated ERC-721 collection.
/// @param signer The initial authorized signer.
/// @param registryOwner The initial registry owner.
event RegistryInitialized(
address indexed hoodiesContract,
address indexed signer,
address indexed registryOwner
);
/// @notice Emitted whenever a holder commits a new Hood Talk.
/// @dev These events form the permanent history for each token.
/// @param tokenId The Hoodie token ID.
/// @param holder The holder who selected the Hood Talk.
/// @param count The token's new cumulative Hood Talk count.
/// @param updatedAt The timestamp when the Hood Talk was committed.
/// @param quote The exact holder-selected Hood Talk.
/// @dev Restricts a function to the registry owner.
modifier onlyOwner() {
if (msg.sender != owner) {
revert NotOwner();
}
_;
}
/// @notice Deploys the Hood Talk Registry.
/// @param hoodies_ Address of the OnChainHoodies ERC-721 contract.
/// @param authorizedSigner_ Address that approves valid Hood Talks.
/// @param owner_ Address that administers the registry.
constructor(
address hoodies_,
address authorizedSigner_,
address owner_
) EIP712("OnChainHoodies Hood Talk", "1") {
if (
hoodies_ == address(0) ||
authorizedSigner_ == address(0) ||
owner_ == address(0)
) {
revert ZeroAddress();
}
hoodies = IERC721(hoodies_);
authorizedSigner = authorizedSigner_;
owner = owner_;
emit OwnershipTransferred(address(0), owner_);
emit AuthorizedSignerUpdated(address(0), authorizedSigner_);
emit RegistryInitialized(
hoodies_,
authorizedSigner_,
owner_
);
}
/// @notice Commits a new holder-selected Hood Talk for a token.
/// @dev The signature must authorize the exact token, current holder,
/// quote hash, next count and deadline.
///
/// The token's existing Hood Talk is replaced in contract state, while
/// the previous Hood Talk remains permanently available in event logs.
///
/// @param tokenId The OnChainHoodies token ID.
/// @param quote The exact Hood Talk approved by the authorized signer.
/// @param deadline Unix timestamp after which the signature expires.
/// @param signature EIP-712 signature created by authorizedSigner.
function setHoodTalk(
uint256 tokenId,
string calldata quote,
uint256 deadline,
bytes calldata signature
) external override {
address holder = hoodies.ownerOf(tokenId);
if (holder != msg.sender) {
revert NotTokenOwner();
}
uint256 quoteLength = bytes(quote).length;
if (
quoteLength == 0 ||
quoteLength > MAX_QUOTE_BYTES
) {
revert InvalidQuoteLength();
}
if (block.timestamp > deadline) {
revert AuthorizationExpired();
}
HoodTalk storage talk = _hoodTalks[tokenId];
_enforceCooldown(talk);
if (talk.count == type(uint32).max) {
revert CountOverflow();
}
uint32 nextCount = talk.count + 1;
_verifySignature(
tokenId,
holder,
keccak256(bytes(quote)),
nextCount,
deadline,
signature
);
if (talk.count == 0) {
unchecked {
++uniqueHoodiesSpoken;
}
}
uint64 updatedAt = uint64(block.timestamp);
talk.quote = quote;
talk.author = holder;
talk.updatedAt = updatedAt;
talk.count = nextCount;
unchecked {
++totalHoodTalks;
}
emit HoodTalkUpdated(
tokenId,
holder,
nextCount,
updatedAt,
quote
);
}
/// @notice Returns the latest Hood Talk stored for a token.
/// @param tokenId The OnChainHoodies token ID.
/// @return quote The latest selected Hood Talk.
/// @return author The holder who selected the latest Hood Talk.
/// @return updatedAt Timestamp when the latest Hood Talk was committed.
/// @return count Total number of Hood Talks committed for the token.
function getHoodTalk(
uint256 tokenId
)
external
view
override
returns (
string memory quote,
address author,
uint64 updatedAt,
uint32 count
)
{
HoodTalk storage talk = _hoodTalks[tokenId];
return (
talk.quote,
talk.author,
talk.updatedAt,
talk.count
);
}
/// @notice Returns only the latest Hood Talk text for a token.
/// @param tokenId The OnChainHoodies token ID.
/// @return The latest Hood Talk, or an empty string if none exists.
function currentQuote(
uint256 tokenId
) external view override returns (string memory) {
return _hoodTalks[tokenId].quote;
}
/// @notice Returns the number of Hood Talks committed for a token.
/// @param tokenId The OnChainHoodies token ID.
/// @return The token's cumulative Hood Talk count.
function hoodTalkCount(
uint256 tokenId
) external view override returns (uint32) {
return _hoodTalks[tokenId].count;
}
/// @notice Returns the earliest timestamp when the token may speak again.
/// @dev The cooldown follows the token and is not reset by transfers.
/// Returns zero when the token has never spoken.
/// @param tokenId The OnChainHoodies token ID.
/// @return The next permitted update timestamp, or zero.
function nextUpdateAt(
uint256 tokenId
) external view override returns (uint256) {
HoodTalk storage talk = _hoodTalks[tokenId];
if (talk.updatedAt == 0) {
return 0;
}
return uint256(talk.updatedAt) + UPDATE_COOLDOWN;
}
/// @notice Returns the exact EIP-712 digest expected by the registry.
/// @dev This helper is useful for backend integration and contract tests.
/// The count must be the token's next Hood Talk count.
/// @param tokenId The OnChainHoodies token ID.
/// @param holder The current owner expected to submit the transaction.
/// @param quote The exact approved Hood Talk.
/// @param count The next Hood Talk count for the token.
/// @param deadline The signature expiry timestamp.
/// @return The EIP-712 digest that the authorized signer must sign.
function authorizationDigest(
uint256 tokenId,
address holder,
string calldata quote,
uint32 count,
uint256 deadline
) external view returns (bytes32) {
return _authorizationDigest(
tokenId,
holder,
keccak256(bytes(quote)),
count,
deadline
);
}
/// @notice Changes the address permitted to authorize Hood Talks.
/// @dev Only the registry owner may call this function.
/// Existing signatures created by the previous signer become invalid.
/// @param newSigner The new authorized signer address.
function setAuthorizedSigner(
address newSigner
) external onlyOwner {
if (newSigner == address(0)) {
revert ZeroAddress();
}
address previousSigner = authorizedSigner;
authorizedSigner = newSigner;
emit AuthorizedSignerUpdated(
previousSigner,
newSigner
);
}
/// @notice Transfers administration of the registry.
/// @dev Only the current registry owner may call this function.
/// @param newOwner The new registry owner address.
function transferOwnership(
address newOwner
) external onlyOwner {
if (newOwner == address(0)) {
revert ZeroAddress();
}
address previousOwner = owner;
owner = newOwner;
emit OwnershipTransferred(
previousOwner,
newOwner
);
}
/// @notice Reports whether this contract implements an interface.
/// @param interfaceId The ERC-165 interface identifier.
/// @return True when interfaceId is supported.
function supportsInterface(
bytes4 interfaceId
) public view virtual override returns (bool) {
return
interfaceId == type(IHoodTalkRegistry).interfaceId ||
super.supportsInterface(interfaceId);
}
/// @dev Enforces the 24-hour cooldown for the token regardless of
/// ownership changes.
function _enforceCooldown(
HoodTalk storage talk
) private view {
if (talk.updatedAt == 0) {
return;
}
uint256 availableAt =
uint256(talk.updatedAt) + UPDATE_COOLDOWN;
if (block.timestamp < availableAt) {
revert CooldownActive(availableAt);
}
}
/// @dev Verifies that authorizedSigner approved the exact submission.
function _verifySignature(
uint256 tokenId,
address holder,
bytes32 quoteHash,
uint32 count,
uint256 deadline,
bytes calldata signature
) private view {
bytes32 digest = _authorizationDigest(
tokenId,
holder,
quoteHash,
count,
deadline
);
if (
digest.recover(signature) !=
authorizedSigner
) {
revert InvalidAuthorization();
}
}
/// @dev Builds the EIP-712 digest for a Hood Talk authorization.
function _authorizationDigest(
uint256 tokenId,
address holder,
bytes32 quoteHash,
uint32 count,
uint256 deadline
) private view returns (bytes32) {
bytes32 structHash = keccak256(
abi.encode(
HOOD_TALK_TYPEHASH,
tokenId,
holder,
quoteHash,
count,
deadline
)
);
return _hashTypedDataV4(structHash);
}
}Chain explorer4604msChain node76ms