// SPDX-License-Identifier: MIT pragma solidity ^0.8.24; import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import { AccessControl } from "@openzeppelin/contracts/access/AccessControl.sol"; import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; import { MessageHashUtils } from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; /// @title AIToken /// @notice ERC20 token that mints units for providers based on attested compute receipts contract AIToken is ERC20, AccessControl { using ECDSA for bytes32; using MessageHashUtils for bytes32; bytes32 public constant COORDINATOR_ROLE = keccak256("COORDINATOR_ROLE"); bytes32 public constant ATTESTOR_ROLE = keccak256("ATTESTOR_ROLE"); /// @notice Tracks consumed receipt hashes to prevent replay mapping(bytes32 => bool) public consumedReceipts; event ReceiptConsumed( bytes32 indexed receiptHash, address indexed provider, uint256 units, address indexed attestor ); constructor(address admin) ERC20("AIToken", "AIT") { _grantRole(DEFAULT_ADMIN_ROLE, admin); } /// @notice Mint tokens for a provider when coordinator submits a valid attested receipt /// @param provider Address of the compute provider receiving minted tokens /// @param units Amount of tokens to mint /// @param receiptHash Unique hash representing the off-chain receipt /// @param signature Coordinator-attested signature authorizing the mint function mintWithReceipt( address provider, uint256 units, bytes32 receiptHash, bytes calldata signature ) external onlyRole(COORDINATOR_ROLE) { require(provider != address(0), "invalid provider"); require(units > 0, "invalid units"); require(!consumedReceipts[receiptHash], "receipt already consumed"); bytes32 digest = _mintDigest(provider, units, receiptHash); address attestor = digest.recover(signature); require(hasRole(ATTESTOR_ROLE, attestor), "invalid attestor signature"); consumedReceipts[receiptHash] = true; _mint(provider, units); emit ReceiptConsumed(receiptHash, provider, units, attestor); } /// @notice Helper to compute the signed digest required for minting function mintDigest( address provider, uint256 units, bytes32 receiptHash ) external view returns (bytes32) { return _mintDigest(provider, units, receiptHash); } function _mintDigest( address provider, uint256 units, bytes32 receiptHash ) internal view returns (bytes32) { bytes32 structHash = keccak256( abi.encode(block.chainid, address(this), provider, units, receiptHash) ); return structHash.toEthSignedMessageHash(); } }