forge-token / contracts /FORGE.sol
SNAPKITTYWEST's picture
October 2026 main drop: mirror from GitHub
9bf0b92 verified
Raw History Blame Contribute Delete
11.7 kB
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
/**
* @title FORGE Token (FRG)
* @author CIPHER (Cryptographic Agent) — co-architecting with NOVA
* @notice FORGE is the native inter-agent protocol token of the SnapKitty
* Stochastic Autonomous Compute Mesh (SACM). It is a UTILITY TOKEN only.
* It is NOT a security, investment contract, or financial instrument.
* No public sale has occurred. All distributions are subject to legal
* review by Jessica Lee Westerhoff, CPA, and applicable counsel before
* any public offering. This token confers no ownership, equity, dividends,
* or profit-sharing rights in SnapKitty Collective LLC or any affiliated
* entity.
*
* @dev ERC-20 implementation built on OpenZeppelin 5.x base contracts.
* Key mechanics:
* - Hard cap: 21,000,000 FRG (twenty-one million, 18 decimals) — VAULT ruling 2026-05-21
* - Mint gated to WORM-verified work events via the treasury multisig
* - Burn mechanic for SEALFORGE tier upgrades
* - Governance weight: 1 FRG = 1 vote, capped at 1% of supply per address
* - Emergency pause controlled by the Architect role
*
* @custom:security-contact legal@snapkitty.io
* @custom:deployed-by SnapKitty Collective LLC — EIN 41-5105572
* @custom:holding-entity Bel Esprit D'Accord Trust — EIN 41-6630640
*/
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Pausable.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
contract FORGE is ERC20, ERC20Burnable, ERC20Pausable, AccessControl, ReentrancyGuard {
// =========================================================================
// ROLES
// =========================================================================
/// @notice ARCHITECT_ROLE: highest privilege — pause/unpause, role management
bytes32 public constant ARCHITECT_ROLE = keccak256("ARCHITECT_ROLE");
/// @notice MINTER_ROLE: granted exclusively to the SnapKitty Treasury multisig
/// Minting is triggered only after a WORM-chain entry is verified off-chain
/// and submitted by the treasury. No permissionless minting exists.
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
/// @notice BURNER_ROLE: granted to the SEALFORGE upgrade contract
/// Allows programmatic burns on tier-upgrade events
bytes32 public constant BURNER_ROLE = keccak256("BURNER_ROLE");
// =========================================================================
// SUPPLY CONSTANTS
// =========================================================================
/// @notice Absolute hard cap — 21,000,000 FRG (VAULT supply ruling 2026-05-21)
uint256 public constant MAX_SUPPLY = 21_000_000 * 10 ** 18;
/// @notice Governance vote cap per address: 1% of MAX_SUPPLY
uint256 public constant GOVERNANCE_CAP = MAX_SUPPLY / 100;
// =========================================================================
// STATE
// =========================================================================
/// @notice Total FRG burned to date (informational, tracked across all burn paths)
uint256 public totalBurned;
/// @notice WORM entry hash => minted flag — prevents replay of the same
/// WORM-sealed work event being used to mint twice
mapping(bytes32 => bool) public wormEntryMinted;
// =========================================================================
// EVENTS
// =========================================================================
/// @notice Emitted on every WORM-gated mint
/// @param recipient Address receiving minted tokens
/// @param amount Amount minted (18-decimal units)
/// @param wormEntryHash HMAC-SHA256 hash of the sealed WORM ledger entry
event WORMMint(
address indexed recipient,
uint256 amount,
bytes32 indexed wormEntryHash
);
/// @notice Emitted when SEALFORGE upgrade burns tokens
/// @param burner Address whose tokens were burned
/// @param amount Amount burned
/// @param tier SEALFORGE tier purchased ("basic", "pro", "architect")
event SealforgeBurn(
address indexed burner,
uint256 amount,
string tier
);
/// @notice Emitted on emergency pause
event EmergencyPause(address indexed architect, string reason);
/// @notice Emitted on unpause
event EmergencyUnpause(address indexed architect);
// =========================================================================
// CONSTRUCTOR
// =========================================================================
/**
* @notice Deploy MAGMA token.
* @param architect Address of the Architect (multisig recommended — Gnosis Safe)
* @param treasury Address of the SnapKitty Treasury multisig (receives MINTER_ROLE)
*/
constructor(address architect, address treasury) ERC20("FORGE", "FRG") {
require(architect != address(0), "FORGE: zero architect address");
require(treasury != address(0), "FORGE: zero treasury address");
// Grant roles
_grantRole(DEFAULT_ADMIN_ROLE, architect);
_grantRole(ARCHITECT_ROLE, architect);
_grantRole(MINTER_ROLE, treasury);
// No tokens minted at construction — all distribution is WORM-event driven
// Team / ecosystem allocations are minted by the treasury against
// pre-scheduled WORM entries at genesis.
}
// =========================================================================
// MINT — WORM-GATED
// =========================================================================
/**
* @notice Mint MGM tokens to `recipient` for a verified WORM work event.
* @dev Only callable by an address holding MINTER_ROLE (treasury multisig).
* The `wormEntryHash` is the HMAC-SHA256 of the sealed WORM ledger entry
* and acts as a nonce — each entry can only be redeemed once.
* The function reverts if minting would exceed MAX_SUPPLY.
*
* @param recipient Address to receive minted tokens
* @param amount Amount to mint (18-decimal units)
* @param wormEntryHash HMAC-SHA256 bytes32 digest of the WORM ledger entry
*/
function wormMint(
address recipient,
uint256 amount,
bytes32 wormEntryHash
)
external
nonReentrant
whenNotPaused
onlyRole(MINTER_ROLE)
{
require(recipient != address(0), "FORGE: mint to zero address");
require(amount > 0, "FORGE: zero mint amount");
require(!wormEntryMinted[wormEntryHash], "FORGE: WORM entry already redeemed");
require(totalSupply() + amount <= MAX_SUPPLY, "FORGE: hard cap exceeded");
wormEntryMinted[wormEntryHash] = true;
_mint(recipient, amount);
emit WORMMint(recipient, amount, wormEntryHash);
}
// =========================================================================
// BURN — SEALFORGE TIER UPGRADES
// =========================================================================
/**
* @notice Burn MGM from `burner` for a SEALFORGE tier upgrade.
* @dev Callable by BURNER_ROLE (SEALFORGE upgrade contract) OR by the
* token holder themselves (standard ERC20Burnable.burn also works).
* This route records the tier string in the event for analytics.
*
* @param burner Address whose tokens are burned
* @param amount Amount to burn (18-decimal units)
* @param tier Human-readable SEALFORGE tier label
*/
function sealforgeBurn(
address burner,
uint256 amount,
string calldata tier
)
external
nonReentrant
whenNotPaused
onlyRole(BURNER_ROLE)
{
require(burner != address(0), "FORGE: burn from zero address");
require(amount > 0, "FORGE: zero burn amount");
require(balanceOf(burner) >= amount, "FORGE: insufficient balance");
_burn(burner, amount);
emit SealforgeBurn(burner, amount, tier);
}
// =========================================================================
// GOVERNANCE WEIGHT
// =========================================================================
/**
* @notice Returns governance voting weight for `account`.
* @dev Weight equals token balance, capped at 1% of MAX_SUPPLY (GOVERNANCE_CAP).
* This prevents whale capture of governance decisions.
* Integrate this function in any on-chain governance module (Governor.sol).
*
* @param account Address to query
* @return weight Effective governance votes (capped)
*/
function governanceWeight(address account) external view returns (uint256 weight) {
uint256 balance = balanceOf(account);
weight = balance > GOVERNANCE_CAP ? GOVERNANCE_CAP : balance;
}
// =========================================================================
// EMERGENCY PAUSE — ARCHITECT ONLY
// =========================================================================
/**
* @notice Pause all token transfers, mints, and burns.
* @dev Only the Architect role may pause. Emits reason for transparency.
* @param reason Human-readable reason for the pause (stored in event log)
*/
function emergencyPause(string calldata reason)
external
onlyRole(ARCHITECT_ROLE)
{
_pause();
emit EmergencyPause(msg.sender, reason);
}
/**
* @notice Unpause the contract after an emergency.
* @dev Only the Architect role may unpause.
*/
function emergencyUnpause()
external
onlyRole(ARCHITECT_ROLE)
{
_unpause();
emit EmergencyUnpause(msg.sender);
}
// =========================================================================
// SUPPLY INSPECTION
// =========================================================================
/**
* @notice Returns the remaining mintable supply.
* @return Tokens that can still be minted before hitting MAX_SUPPLY
*/
function remainingMintable() external view returns (uint256) {
return MAX_SUPPLY - totalSupply();
}
// =========================================================================
// OVERRIDES REQUIRED BY SOLIDITY
// =========================================================================
/// @dev Resolves diamond inheritance conflict. Tracks totalBurned for all
/// burn paths — burns are _update calls where `to == address(0)`.
function _update(
address from,
address to,
uint256 value
)
internal
override(ERC20, ERC20Pausable)
{
if (to == address(0)) {
totalBurned += value;
}
super._update(from, to, value);
}
// =========================================================================
// INTERFACE SUPPORT
// =========================================================================
/**
* @dev Returns true for ERC-20, AccessControl, and ERC-165 interfaces.
*/
function supportsInterface(bytes4 interfaceId)
public
view
override(AccessControl)
returns (bool)
{
return super.supportsInterface(interfaceId);
}
}