// SPDX-License-Identifier: MIT pragma solidity ^0.8.24; /// @title STANK Launch Token /// @notice A fixed-supply, unrestricted ERC-20 deployed by the STANK launch factory. /// @dev There are no owner privileges, taxes, blacklists, trading switches, or post-deployment mint paths. contract StankLaunchToken { string public name; string public symbol; uint8 public constant decimals = 18; /// @notice On-chain attribution: every STANK token is launched from stank.fun. Immutable. string public constant LAUNCHPAD = "stank.fun"; /// @notice The STANK launch factory that deployed this token; the only address that may set metadata. address public immutable launchpad; /// @notice Off-chain metadata URI (image + socials JSON) for this token, set once at launch. string public metadataURI; /// @dev Mutable because `burn` reduces it. Only ever decreases after construction, so it /// stays within the factory's `uint128` cap and the transfer overflow invariant holds. uint256 public totalSupply; mapping(address account => uint256 balance) public balanceOf; mapping(address owner => mapping(address spender => uint256 amount)) public allowance; error ZeroAddress(); error InsufficientBalance(); error InsufficientAllowance(); error NotLaunchpad(); error MetadataAlreadySet(); event Transfer(address indexed from, address indexed to, uint256 value); event Approval(address indexed owner, address indexed spender, uint256 value); event MetadataSet(string uri); /// @param name_ ERC-20 display name. /// @param symbol_ ERC-20 ticker. /// @param supply_ Fixed supply, expressed in 18-decimal base units. /// @param initialHolder_ Address that receives the entire fixed supply. constructor(string memory name_, string memory symbol_, uint256 supply_, address initialHolder_) { if (initialHolder_ == address(0)) revert ZeroAddress(); launchpad = msg.sender; name = name_; symbol = symbol_; totalSupply = supply_; balanceOf[initialHolder_] = supply_; emit Transfer(address(0), initialHolder_, supply_); } /// @notice Sets this token's image + socials metadata URI. Callable once, only by the launchpad. /// @dev Lets explorers and the stank.fun app display the token's image and social links. Leaving it /// empty (no call) is allowed; the token still carries the immutable `LAUNCHPAD` attribution. function setMetadata(string calldata uri) external { if (msg.sender != launchpad) revert NotLaunchpad(); if (bytes(metadataURI).length != 0) revert MetadataAlreadySet(); metadataURI = uri; emit MetadataSet(uri); } /// @notice Transfers tokens without launch-specific restrictions. function transfer(address to, uint256 amount) external returns (bool) { _transfer(msg.sender, to, amount); return true; } /// @notice Approves a spender according to ERC-20 semantics. function approve(address spender, uint256 amount) external returns (bool) { allowance[msg.sender][spender] = amount; emit Approval(msg.sender, spender, amount); return true; } /// @notice Transfers tokens using an allowance. function transferFrom(address from, address to, uint256 amount) external returns (bool) { uint256 allowed = allowance[from][msg.sender]; if (allowed != type(uint256).max) { if (allowed < amount) revert InsufficientAllowance(); unchecked { allowance[from][msg.sender] = allowed - amount; } emit Approval(from, msg.sender, allowance[from][msg.sender]); } _transfer(from, to, amount); return true; } /// @notice Permanently destroys `amount` of the caller's tokens, reducing total supply. /// @dev Used to burn the launch-token trading fees the permanent locker collects, making the /// token deflationary. Burning only lowers `totalSupply`, so every supply-dependent /// invariant (the factory's uint128 cap, the transfer overflow bound) is preserved. function burn(uint256 amount) external { uint256 fromBalance = balanceOf[msg.sender]; if (fromBalance < amount) revert InsufficientBalance(); unchecked { balanceOf[msg.sender] = fromBalance - amount; totalSupply -= amount; } emit Transfer(msg.sender, address(0), amount); } function _transfer(address from, address to, uint256 amount) private { if (to == address(0)) revert ZeroAddress(); uint256 fromBalance = balanceOf[from]; if (fromBalance < amount) revert InsufficientBalance(); unchecked { balanceOf[from] = fromBalance - amount; balanceOf[to] += amount; } emit Transfer(from, to, amount); } }