Blockchain / Global / Address
Contract
0x19bd23A04d604d5B361bfb348F4133b22d0D46FC
- Balance
- 15 SOMI
- Transactions sent
- 1
- Type
- Contract · 6,570 bytes of code
SomniaQuestBadge Verified
- Compiler
- solc 0.8.24+commit.e11b9ed9
- Language
- Solidity
- Optimization
- Yes, 200 runs
- EVM version
- paris
- License
- —
- Verified
- 2026-10-12
contracts/SomniaQuestBadge.sol
Solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
/**
* @title SomniaQuestBadge
* @dev ERC-721-like "Quest Badge" NFT with signature-gated minting and paid mint.
*
* Goal: Prevent users from minting directly via explorer/contract calls unless
* they have a valid server signature (issued only after completing SomniaQuests),
* and charge a SOMI fee per mint.
*
* - One badge per address (hasMinted).
* - Admin-controlled signer address (off-chain backend signer).
* - TokenURI is a single shared URI (badge metadata), settable by owner.
* - Mint has a configurable SOMI price; only owner can withdraw funds.
*
* Signature scheme matches backend:
* messageHash = keccak256(abi.encode(
* "SomniaQuestBadge:mint",
* address(this),
* block.chainid,
* msg.sender,
* deadline
* ));
* prefixedHash = keccak256("\x19Ethereum Signed Message:\n32" + messageHash);
*/
contract SomniaQuestBadge {
// --- ERC-721 minimal storage ---
string public name;
string public symbol;
string private _tokenURI;
address public owner;
address public signer; // backend signer address
uint256 private _totalSupply;
uint256 private _nextTokenId;
mapping(uint256 => address) private _owners;
mapping(address => uint256) private _balances;
mapping(uint256 => address) private _tokenApprovals;
mapping(address => mapping(address => bool)) private _operatorApprovals;
// One per address
mapping(address => bool) public hasMinted;
// Mint pricing (in SOMI, 18 decimals)
uint256 public mintPrice; // e.g. 0.1 ether now, 5 ether later
// --- Events ---
event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);
event ApprovalForAll(address indexed owner, address indexed operator, bool approved);
event SignerUpdated(address indexed newSigner);
event TokenURIUpdated(string newTokenURI);
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
event MintPriceUpdated(uint256 newMintPrice);
// --- Constants ---
string private constant _ACTION = "SomniaQuestBadge:mint";
// --- Constructor ---
constructor(address signer_, string memory tokenURI_) {
require(signer_ != address(0), "Zero signer");
owner = msg.sender;
signer = signer_;
name = "Somnia Quest Badge";
symbol = "SQB";
_tokenURI = tokenURI_;
_nextTokenId = 1;
// Initial mint price: 0.1 SOMI (can be changed later via setMintPrice)
mintPrice = 0.1 ether;
emit SignerUpdated(signer_);
emit TokenURIUpdated(tokenURI_);
emit OwnershipTransferred(address(0), msg.sender);
emit MintPriceUpdated(mintPrice);
}
// --- Modifiers ---
modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
_;
}
// --- Admin setters ---
function setSigner(address newSigner) external onlyOwner {
require(newSigner != address(0), "Zero signer");
signer = newSigner;
emit SignerUpdated(newSigner);
}
function setTokenURI(string calldata newTokenURI) external onlyOwner {
_tokenURI = newTokenURI;
emit TokenURIUpdated(newTokenURI);
}
function transferOwnership(address newOwner) external onlyOwner {
require(newOwner != address(0), "Zero owner");
emit OwnershipTransferred(owner, newOwner);
owner = newOwner;
}
/**
* @dev Set mint price in SOMI (18 decimals).
* Example:
* - 0.1 SOMI => newPrice = 0.1 ether
* - 5 SOMI => newPrice = 5 ether
*/
function setMintPrice(uint256 newPrice) external onlyOwner {
mintPrice = newPrice;
emit MintPriceUpdated(newPrice);
}
// --- Mint ---
function mint(uint256 deadline, bytes calldata signature) external payable returns (uint256) {
require(block.timestamp <= deadline, "Signature expired");
require(!hasMinted[msg.sender], "Already minted");
require(_verify(msg.sender, deadline, signature), "Invalid signature");
// Paid mint check
require(mintPrice > 0, "Mint price not set");
require(msg.value >= mintPrice, "Insufficient payment");
// Optional: refund any overpayment
if (msg.value > mintPrice) {
uint256 refundAmount = msg.value - mintPrice;
(bool refunded, ) = payable(msg.sender).call{value: refundAmount}("");
require(refunded, "Refund failed");
}
hasMinted[msg.sender] = true;
uint256 tokenId = _mintTo(msg.sender);
return tokenId;
}
function _verify(address minter, uint256 deadline, bytes calldata signature) internal view returns (bool) {
bytes32 messageHash = keccak256(abi.encode(_ACTION, address(this), block.chainid, minter, deadline));
bytes32 prefixedHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", messageHash));
return _recover(prefixedHash, signature) == signer;
}
function _mintTo(address to) internal returns (uint256) {
uint256 tokenId = _nextTokenId;
_nextTokenId++;
_totalSupply++;
_owners[tokenId] = to;
_balances[to] += 1;
emit Transfer(address(0), to, tokenId);
return tokenId;
}
// --- Withdraw (treasury) ---
/**
* @dev Owner can withdraw a specific amount of collected mint fees.
*/
function withdraw(address payable to, uint256 amount) external onlyOwner {
require(to != address(0), "Zero address");
require(amount > 0, "Zero amount");
require(amount <= address(this).balance, "Insufficient balance");
(bool ok, ) = to.call{value: amount}("");
require(ok, "Withdraw failed");
}
/**
* @dev Owner can withdraw full contract balance.
*/
function withdrawAll(address payable to) external onlyOwner {
require(to != address(0), "Zero address");
uint256 balance = address(this).balance;
require(balance > 0, "No balance");
(bool ok, ) = to.call{value: balance}("");
require(ok, "Withdraw failed");
}
// --- Views ---
function totalSupply() external view returns (uint256) {
return _totalSupply;
}
function tokenURI(uint256 tokenId) external view returns (string memory) {
require(_owners[tokenId] != address(0), "Token does not exist");
return _tokenURI;
}
function ownerOf(uint256 tokenId) public view returns (address) {
address tokenOwner = _owners[tokenId];
require(tokenOwner != address(0), "Token does not exist");
return tokenOwner;
}
function balanceOf(address addr) external view returns (uint256) {
require(addr != address(0), "Zero address");
return _balances[addr];
}
function supportsInterface(bytes4 interfaceId) external pure returns (bool) {
return
interfaceId == 0x80ac58cd || // ERC-721
interfaceId == 0x01ffc9a7 || // ERC-165
interfaceId == 0x5b5e139f; // ERC-721Metadata
}
// --- Approvals ---
function approve(address to, uint256 tokenId) external {
address tokenOwner = ownerOf(tokenId);
require(msg.sender == tokenOwner || _operatorApprovals[tokenOwner][msg.sender], "Not authorized");
_tokenApprovals[tokenId] = to;
emit Approval(tokenOwner, to, tokenId);
}
function getApproved(uint256 tokenId) external view returns (address) {
require(_owners[tokenId] != address(0), "Token does not exist");
return _tokenApprovals[tokenId];
}
function setApprovalForAll(address operator, bool approved) external {
_operatorApprovals[msg.sender][operator] = approved;
emit ApprovalForAll(msg.sender, operator, approved);
}
function isApprovedForAll(address addr, address operator) external view returns (bool) {
return _operatorApprovals[addr][operator];
}
// --- Transfers (minimal, not fully safeTransfer) ---
function transferFrom(address from, address to, uint256 tokenId) public {
require(_owners[tokenId] == from, "Not owner");
require(to != address(0), "Zero address");
require(
msg.sender == from ||
_tokenApprovals[tokenId] == msg.sender ||
_operatorApprovals[from][msg.sender],
"Not authorized"
);
_balances[from] -= 1;
_balances[to] += 1;
_owners[tokenId] = to;
delete _tokenApprovals[tokenId];
emit Transfer(from, to, tokenId);
}
function safeTransferFrom(address from, address to, uint256 tokenId) external {
transferFrom(from, to, tokenId);
}
function safeTransferFrom(address from, address to, uint256 tokenId, bytes calldata) external {
transferFrom(from, to, tokenId);
}
// --- Signature recovery ---
function _recover(bytes32 digest, bytes calldata sig) internal pure returns (address) {
if (sig.length != 65) return address(0);
bytes32 r;
bytes32 s;
uint8 v;
// solhint-disable-next-line no-inline-assembly
assembly {
r := calldataload(sig.offset)
s := calldataload(add(sig.offset, 32))
v := byte(0, calldataload(add(sig.offset, 64)))
}
if (v < 27) v += 27;
if (v != 27 && v != 28) return address(0);
// Enforce low-s malleability per EIP-2
if (uint256(s) > 0x7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a0) {
return address(0);
}
return ecrecover(digest, v, r, s);
}
// --- Receive ETH/SOMI directly ---
receive() external payable {}
}
ABI · 34 entries
json
[
{
"type": "constructor",
"inputs": [
{
"name": "signer_",
"type": "address",
"internalType": "address"
},
{
"name": "tokenURI_",
"type": "string",
"internalType": "string"
}
],
"stateMutability": "nonpayable"
},
{
"name": "Approval",
"type": "event",
"inputs": [
{
"name": "owner",
"type": "address",
"indexed": true,
"internalType": "address"
},
{
"name": "approved",
"type": "address",
"indexed": true,
"internalType": "address"
},
{
"name": "tokenId",
"type": "uint256",
"indexed": true,
"internalType": "uint256"
}
],
"anonymous": false
},
{
"name": "ApprovalForAll",
"type": "event",
"inputs": [
{
"name": "owner",
"type": "address",
"indexed": true,
"internalType": "address"
},
{
"name": "operator",
"type": "address",
"indexed": true,
"internalType": "address"
},
{
"name": "approved",
"type": "bool",
"indexed": false,
"internalType": "bool"
}
],
"anonymous": false
},
{
"name": "MintPriceUpdated",
"type": "event",
"inputs": [
{
"name": "newMintPrice",
"type": "uint256",
"indexed": false,
"internalType": "uint256"
}
],
"anonymous": false
},
{
"name": "OwnershipTransferred",
"type": "event",
"inputs": [
{
"name": "previousOwner",
"type": "address",
"indexed": true,
"internalType": "address"
},
{
"name": "newOwner",
"type": "address",
"indexed": true,
"internalType": "address"
}
],
"anonymous": false
},
{
"name": "SignerUpdated",
"type": "event",
"inputs": [
{
"name": "newSigner",
"type": "address",
"indexed": true,
"internalType": "address"
}
],
"anonymous": false
},
{
"name": "TokenURIUpdated",
"type": "event",
"inputs": [
{
"name": "newTokenURI",
"type": "string",
"indexed": false,
"internalType": "string"
}
],
"anonymous": false
},
{
"name": "Transfer",
"type": "event",
"inputs": [
{
"name": "from",
"type": "address",
"indexed": true,
"internalType": "address"
},
{
"name": "to",
"type": "address",
"indexed": true,
"internalType": "address"
},
{
"name": "tokenId",
"type": "uint256",
"indexed": true,
"internalType": "uint256"
}
],
"anonymous": false
},
{
"name": "approve",
"type": "function",
"inputs": [
{
"name": "to",
"type": "address",
"internalType": "address"
},
{
"name": "tokenId",
"type": "uint256",
"internalType": "uint256"
}
],
"outputs": [],
"stateMutability": "nonpayable"
},
{
"name": "balanceOf",
"type": "function",
"inputs": [
{
"name": "addr",
"type": "address",
"internalType": "address"
}
],
"outputs": [
{
"name": "",
"type": "uint256",
"internalType": "uint256"
}
],
"stateMutability": "view"
},
{
"name": "getApproved",
"type": "function",
"inputs": [
{
"name": "tokenId",
"type": "uint256",
"internalType": "uint256"
}
],
"outputs": [
{
"name": "",
"type": "address",
"internalType": "address"
}
],
"stateMutability": "view"
},
{
"name": "hasMinted",
"type": "function",
"inputs": [
{
"name": "",
"type": "address",
"internalType": "address"
}
],
"outputs": [
{
"name": "",
"type": "bool",
"internalType": "bool"
}
],
"stateMutability": "view"
},
{
"name": "isApprovedForAll",
"type": "function",
"inputs": [
{
"name": "addr",
"type": "address",
"internalType": "address"
},
{
"name": "operator",
"type": "address",
"internalType": "address"
}
],
"outputs": [
{
"name": "",
"type": "bool",
"internalType": "bool"
}
],
"stateMutability": "view"
},
{
"name": "mint",
"type": "function",
"inputs": [
{
"name": "deadline",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "signature",
"type": "bytes",
"internalType": "bytes"
}
],
"outputs": [
{
"name": "",
"type": "uint256",
"internalType": "uint256"
}
],
"stateMutability": "payable"
},
{
"name": "mintPrice",
"type": "function",
"inputs": [],
"outputs": [
{
"name": "",
"type": "uint256",
"internalType": "uint256"
}
],
"stateMutability": "view"
},
{
"name": "name",
"type": "function",
"inputs": [],
"outputs": [
{
"name": "",
"type": "string",
"internalType": "string"
}
],
"stateMutability": "view"
},
{
"name": "owner",
"type": "function",
"inputs": [],
"outputs": [
{
"name": "",
"type": "address",
"internalType": "address"
}
],
"stateMutability": "view"
},
{
"name": "ownerOf",
"type": "function",
"inputs": [
{
"name": "tokenId",
"type": "uint256",
"internalType": "uint256"
}
],
"outputs": [
{
"name": "",
"type": "address",
"internalType": "address"
}
],
"stateMutability": "view"
},
{
"name": "safeTransferFrom",
"type": "function",
"inputs": [
{
"name": "from",
"type": "address",
"internalType": "address"
},
{
"name": "to",
"type": "address",
"internalType": "address"
},
{
"name": "tokenId",
"type": "uint256",
"internalType": "uint256"
}
],
"outputs": [],
"stateMutability": "nonpayable"
},
{
"name": "safeTransferFrom",
"type": "function",
"inputs": [
{
"name": "from",
"type": "address",
"internalType": "address"
},
{
"name": "to",
"type": "address",
"internalType": "address"
},
{
"name": "tokenId",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "",
"type": "bytes",
"internalType": "bytes"
}
],
"outputs": [],
"stateMutability": "nonpayable"
},
{
"name": "setApprovalForAll",
"type": "function",
"inputs": [
{
"name": "operator",
"type": "address",
"internalType": "address"
},
{
"name": "approved",
"type": "bool",
"internalType": "bool"
}
],
"outputs": [],
"stateMutability": "nonpayable"
},
{
"name": "setMintPrice",
"type": "function",
"inputs": [
{
"name": "newPrice",
"type": "uint256",
"internalType": "uint256"
}
],
"outputs": [],
"stateMutability": "nonpayable"
},
{
"name": "setSigner",
"type": "function",
"inputs": [
{
"name": "newSigner",
"type": "address",
"internalType": "address"
}
],
"outputs": [],
"stateMutability": "nonpayable"
},
{
"name": "setTokenURI",
"type": "function",
"inputs": [
{
"name": "newTokenURI",
"type": "string",
"internalType": "string"
}
],
"outputs": [],
"stateMutability": "nonpayable"
},
{
"name": "signer",
"type": "function",
"inputs": [],
"outputs": [
{
"name": "",
"type": "address",
"internalType": "address"
}
],
"stateMutability": "view"
},
{
"name": "supportsInterface",
"type": "function",
"inputs": [
{
"name": "interfaceId",
"type": "bytes4",
"internalType": "bytes4"
}
],
"outputs": [
{
"name": "",
"type": "bool",
"internalType": "bool"
}
],
"stateMutability": "pure"
},
{
"name": "symbol",
"type": "function",
"inputs": [],
"outputs": [
{
"name": "",
"type": "string",
"internalType": "string"
}
],
"stateMutability": "view"
},
{
"name": "tokenURI",
"type": "function",
"inputs": [
{
"name": "tokenId",
"type": "uint256",
"internalType": "uint256"
}
],
"outputs": [
{
"name": "",
"type": "string",
"internalType": "string"
}
],
"stateMutability": "view"
},
{
"name": "totalSupply",
"type": "function",
"inputs": [],
"outputs": [
{
"name": "",
"type": "uint256",
"internalType": "uint256"
}
],
"stateMutability": "view"
},
{
"name": "transferFrom",
"type": "function",
"inputs": [
{
"name": "from",
"type": "address",
"internalType": "address"
},
{
"name": "to",
"type": "address",
"internalType": "address"
},
{
"name": "tokenId",
"type": "uint256",
"internalType": "uint256"
}
],
"outputs": [],
"stateMutability": "nonpayable"
},
{
"name": "transferOwnership",
"type": "function",
"inputs": [
{
"name": "newOwner",
"type": "address",
"internalType": "address"
}
],
"outputs": [],
"stateMutability": "nonpayable"
},
{
"name": "withdraw",
"type": "function",
"inputs": [
{
"name": "to",
"type": "address",
"internalType": "address payable"
},
{
"name": "amount",
"type": "uint256",
"internalType": "uint256"
}
],
"outputs": [],
"stateMutability": "nonpayable"
},
{
"name": "withdrawAll",
"type": "function",
"inputs": [
{
"name": "to",
"type": "address",
"internalType": "address payable"
}
],
"outputs": [],
"stateMutability": "nonpayable"
},
{
"type": "receive",
"stateMutability": "payable"
}
]