pAIToken

Description:

Decentralized Finance (DeFi) protocol contract providing Mintable, Factory functionality.

Blockchain: Ethereum

Source Code: View Code On The Blockchain

Solidity Source Code:

{{
  "language": "Solidity",
  "sources": {
    "contracts/pAIToken.sol": {
      "content": "// SPDX-License-Identifier: MIT
pragma solidity 0.8.30;

//@title pAI Token - Mineable AI-themed ERC20 Token using Proof Of Work + Linear Vesting
//@notice Symbol: pAI | Name: Proof of AI | Decimals: 18
//@dev Total supply: 150,000,000 pAI (66.67% initial allocation, 33.33% POW mined)

// ============================================================================
// Custom Errors (Gas Optimized)
// ============================================================================
error InvalidProofOfWork();
error AlreadyMinedInBlock();
error MaxSupplyExceeded();
error PermitExpired();
error InvalidSignature();
error ETHNotAccepted();
error ZeroAddress();
error InsufficientBalance();
error InsufficientAllowance();
error TooSoonToClaim();
error InsufficientHashPower();
error VestingPoolDepleted();

// ============================================================================
// Interfaces
// ============================================================================

interface IERC20 {
    event Transfer(address indexed from, address indexed to, uint256 value);
    event Approval(
        address indexed owner,
        address indexed spender,
        uint256 value
    );

    function totalSupply() external view returns (uint256);
    function balanceOf(address account) external view returns (uint256);
    function transfer(address to, uint256 amount) external returns (bool);
    function allowance(
        address owner,
        address spender
    ) external view returns (uint256);
    function approve(address spender, uint256 amount) external returns (bool);
    function transferFrom(
        address from,
        address to,
        uint256 amount
    ) external returns (bool);
}

interface IERC20Metadata is IERC20 {
    function name() external view returns (string memory);
    function symbol() external view returns (string memory);
    function decimals() external view returns (uint8);
}

interface IERC20Permit {
    function permit(
        address owner,
        address spender,
        uint256 value,
        uint256 deadline,
        uint8 v,
        bytes32 r,
        bytes32 s
    ) external;

    function nonces(address owner) external view returns (uint256);
    function DOMAIN_SEPARATOR() external view returns (bytes32);
}

// ============================================================================
// Libraries
// ============================================================================

library ExtendedMath {
    function min(uint256 a, uint256 b) internal pure returns (uint256) {
        return a < b ? a : b;
    }

    function limitLessThan(
        uint256 a,
        uint256 b
    ) internal pure returns (uint256) {
        return a > b ? b : a;
    }
}

library ECRecover {
    function recover(
        bytes32 digest,
        uint8 v,
        bytes32 r,
        bytes32 s
    ) internal pure returns (address) {
        if (
            uint256(s) >
            0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0
        ) {
            revert InvalidSignature();
        }
        if (v != 27 && v != 28) {
            revert InvalidSignature();
        }

        address signer = ecrecover(digest, v, r, s);
        if (signer == address(0)) {
            revert InvalidSignature();
        }

        return signer;
    }
}

library EIP712 {
    function makeDomainSeparator(
        string memory name,
        string memory version,
        address verifyingContract
    ) internal view returns (bytes32) {
        return
            keccak256(
                abi.encode(
                    keccak256(
                        "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"
                    ),
                    keccak256(bytes(name)),
                    keccak256(bytes(version)),
                    block.chainid,
                    verifyingContract
                )
            );
    }

    function recover(
        bytes32 domainSeparator,
        uint8 v,
        bytes32 r,
        bytes32 s,
        bytes memory typeHashAndData
    ) internal pure returns (address) {
        bytes32 digest = keccak256(
            abi.encodePacked(
                "\x19\x01",
                domainSeparator,
                keccak256(typeHashAndData)
            )
        );
        return ECRecover.recover(digest, v, r, s);
    }
}

// ============================================================================
// Main Contract
// ============================================================================

contract pAIToken is IERC20, IERC20Metadata, IERC20Permit {
    using ExtendedMath for uint256;

    // ========================================================================
    // State Variables
    // ========================================================================

    // ERC20 Basic
    string public constant name = "Proof of AI";
    string public constant symbol = "pAI";
    uint8 public constant decimals = 18;
    string public constant version = "1";

    uint256 public totalSupply;
    mapping(address => uint256) private _balances;
    mapping(address => mapping(address => uint256)) private _allowances;

    // EIP-2612 Permit
    bytes32 public immutable DOMAIN_SEPARATOR;
    bytes32 public constant PERMIT_TYPEHASH =
        keccak256(
            "Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"
        );
    mapping(address => uint256) private _nonces;

    // ========================================================================
    // Supply Distribution Constants
    // ========================================================================

    uint256 public constant MAXIMUM_SUPPLY = 150_000_000 * 10 ** 18; // 150M pAI
    uint256 public constant INITIAL_ALLOCATION = 100_000_000 * 10 ** 18; // 100M pAI (66.67%)
    uint256 public constant MINEABLE_SUPPLY = 50_000_000 * 10 ** 18; // 50M pAI (33.33%)

    /// @notice Traditional PoW allocation (instant rewards)
    uint256 public constant POW_ALLOCATION = 40_000_000 * 10 ** 18; // 40M pAI (80% of mineable)

    /// @notice Linear vesting pool allocation
    uint256 public constant VESTING_POOL_ALLOCATION = 10_000_000 * 10 ** 18; // 10M pAI (20% of mineable)

    // Total mineable: 40M + 10M = 50M ✅

    // ========================================================================
    // POW Mining State
    // ========================================================================

    uint256 public constant BLOCKS_PER_READJUSTMENT = 2048;
    uint256 public constant MINIMUM_TARGET = 2 ** 16;
    uint256 public constant MAXIMUM_TARGET = 2 ** 234;
    uint256 public constant TARGET_BLOCKS_PER_PERIOD =
        BLOCKS_PER_READJUSTMENT * 40;

    uint256 public miningTarget;
    bytes32 public challengeNumber;
    uint256 public epochCount;
    uint256 public rewardEra;
    uint256 public maxSupplyForEra;
    uint256 public currentMiningReward;
    uint256 public tokensMinted;
    uint256 public latestDifficultyPeriodStarted;
    uint256 public lastRewardEthBlockNumber;

    address public lastRewardTo;
    uint256 public lastRewardAmount;

    address public immutable initialRecipient;

    // ========================================================================
    // Linear Vesting Mining System
    // ========================================================================

    uint256 public constant DAILY_VESTING_REWARD = 0.5 * 10 ** 18;
    uint256 public constant REQUIRED_DAILY_HASH_POWER = 10;

    uint256 public totalVestingClaimed;
    uint256 public totalActiveMiners;

    struct MinerStake {
        uint256 lastClaimTime;
        uint256 accumulatedHashPower;
        uint256 totalVestingClaimed;
        uint256 totalHashPowerSubmitted;
        uint256 firstSubmissionTime;
    }

    mapping(address => MinerStake) public minerStakes;

    // ========================================================================
    // Events
    // ========================================================================

    event Mint(
        address indexed miner,
        uint256 reward,
        uint256 epochCount,
        bytes32 newChallengeNumber
    );
    event DifficultyAdjusted(
        uint256 oldTarget,
        uint256 newTarget,
        uint256 ethBlocksSinceLastAdjustment
    );
    event EraTransition(uint256 newEra, uint256 newReward);
    event InitialAllocation(address indexed recipient, uint256 amount);
    event HashPowerSubmitted(
        address indexed miner,
        uint256 hashPower,
        uint256 totalHashPower
    );
    event VestingRewardClaimed(
        address indexed miner,
        uint256 amount,
        uint256 daysClaimed,
        uint256 hashPowerUsed
    );

    // ========================================================================
    // Constructor
    // ========================================================================

    constructor(address _initialRecipient) {
        if (_initialRecipient == address(0)) revert ZeroAddress();

        initialRecipient = _initialRecipient;

        DOMAIN_SEPARATOR = EIP712.makeDomainSeparator(
            name,
            version,
            address(this)
        );

        _mint(_initialRecipient, INITIAL_ALLOCATION);
        emit InitialAllocation(_initialRecipient, INITIAL_ALLOCATION);

        miningTarget = MAXIMUM_TARGET;
        challengeNumber = blockhash(block.number - 1);
        rewardEra = 0;
        currentMiningReward = 100 * 10 ** decimals;
        maxSupplyForEra = INITIAL_ALLOCATION + (POW_ALLOCATION / 2); // 100M + 20M = 120M
        latestDifficultyPeriodStarted = block.number;
        epochCount = 0;
        tokensMinted = 0;
    }

    // ========================================================================
    // ERC20 Standard Functions
    // ========================================================================

    function balanceOf(address account) public view override returns (uint256) {
        return _balances[account];
    }

    function transfer(
        address to,
        uint256 amount
    ) public override returns (bool) {
        _transfer(msg.sender, to, amount);
        return true;
    }

    function allowance(
        address owner,
        address spender
    ) public view override returns (uint256) {
        return _allowances[owner][spender];
    }

    function approve(
        address spender,
        uint256 amount
    ) public override returns (bool) {
        _approve(msg.sender, spender, amount);
        return true;
    }

    function transferFrom(
        address from,
        address to,
        uint256 amount
    ) public override returns (bool) {
        uint256 currentAllowance = _allowances[from][msg.sender];
        if (currentAllowance < amount) revert InsufficientAllowance();

        unchecked {
            _approve(from, msg.sender, currentAllowance - amount);
        }
        _transfer(from, to, amount);
        return true;
    }

    function _transfer(address from, address to, uint256 amount) internal {
        if (from == address(0)) revert ZeroAddress();
        if (to == address(0)) revert ZeroAddress();
        if (_balances[from] < amount) revert InsufficientBalance();

        unchecked {
            _balances[from] -= amount;
            _balances[to] += amount;
        }

        emit Transfer(from, to, amount);
    }

    function _approve(address owner, address spender, uint256 amount) internal {
        if (owner == address(0)) revert ZeroAddress();
        if (spender == address(0)) revert ZeroAddress();

        _allowances[owner][spender] = amount;
        emit Approval(owner, spender, amount);
    }

    function _mint(address account, uint256 amount) internal {
        if (account == address(0)) revert ZeroAddress();

        unchecked {
            totalSupply += amount;
            _balances[account] += amount;
        }

        emit Transfer(address(0), account, amount);
    }

    // ========================================================================
    // EIP-2612 Permit Functions
    // ========================================================================

    function nonces(address owner) public view override returns (uint256) {
        return _nonces[owner];
    }

    function permit(
        address owner,
        address spender,
        uint256 value,
        uint256 deadline,
        uint8 v,
        bytes32 r,
        bytes32 s
    ) external override {
        if (block.timestamp > deadline) revert PermitExpired();

        bytes memory data = abi.encode(
            PERMIT_TYPEHASH,
            owner,
            spender,
            value,
            _nonces[owner]++,
            deadline
        );

        address recoveredAddress = EIP712.recover(
            DOMAIN_SEPARATOR,
            v,
            r,
            s,
            data
        );
        if (recoveredAddress != owner) revert InvalidSignature();

        _approve(owner, spender, value);
    }

    // ========================================================================
    // POW Mining Functions (Traditional)
    // ========================================================================

    function mint(uint256 nonce) external returns (bool success) {
        return _mintTo(nonce, msg.sender);
    }

    function mintTo(
        uint256 nonce,
        address minter
    ) external returns (bool success) {
        return _mintTo(nonce, minter);
    }

    function _mintTo(uint256 nonce, address minter) internal returns (bool) {
        if (minter == address(0)) revert ZeroAddress();

        bytes32 digest = keccak256(
            abi.encodePacked(
                keccak256(abi.encodePacked(challengeNumber, minter, nonce))
            )
        );

        if (uint256(digest) > miningTarget) revert InvalidProofOfWork();
        if (lastRewardEthBlockNumber == block.number)
            revert AlreadyMinedInBlock();
        if (tokensMinted + currentMiningReward > maxSupplyForEra)
            revert MaxSupplyExceeded();

        _mint(minter, currentMiningReward);

        unchecked {
            tokensMinted += currentMiningReward;
        }

        lastRewardTo = minter;
        lastRewardAmount = currentMiningReward;
        lastRewardEthBlockNumber = block.number;

        _startNewMiningEpoch();

        emit Mint(minter, currentMiningReward, epochCount, challengeNumber);

        return true;
    }

    function _startNewMiningEpoch() internal {
        if (
            totalSupply + currentMiningReward > maxSupplyForEra && rewardEra < 7
        ) {
            unchecked {
                rewardEra++;
            }

            currentMiningReward = currentMiningReward / 2;
            maxSupplyForEra =
                maxSupplyForEra +
                (POW_ALLOCATION / (2 ** (rewardEra + 1)));

            emit EraTransition(rewardEra, currentMiningReward);
        }

        unchecked {
            epochCount++;
        }

        if (epochCount % BLOCKS_PER_READJUSTMENT == 0) {
            uint256 ethBlocksSinceLastAdjustment;
            unchecked {
                ethBlocksSinceLastAdjustment =
                    block.number -
                    latestDifficultyPeriodStarted;
            }
            _reAdjustDifficulty(ethBlocksSinceLastAdjustment);
        }

        challengeNumber = blockhash(block.number - 1);
    }

    function _reAdjustDifficulty(uint256 ethBlocksSinceLastPeriod) internal {
        uint256 oldTarget = miningTarget;

        if (ethBlocksSinceLastPeriod < TARGET_BLOCKS_PER_PERIOD) {
            uint256 excessBlockPct = (TARGET_BLOCKS_PER_PERIOD * 100) /
                ethBlocksSinceLastPeriod;
            uint256 excessBlockPctExtra = (excessBlockPct - 100).limitLessThan(
                1000
            );

            unchecked {
                miningTarget -= (miningTarget / 2000) * excessBlockPctExtra;
            }
        } else {
            uint256 shortageBlockPct = (ethBlocksSinceLastPeriod * 100) /
                TARGET_BLOCKS_PER_PERIOD;
            uint256 shortageBlockPctExtra = (shortageBlockPct - 100)
                .limitLessThan(1000);

            unchecked {
                miningTarget += (miningTarget / 2000) * shortageBlockPctExtra;
            }
        }

        if (miningTarget < MINIMUM_TARGET) {
            miningTarget = MINIMUM_TARGET;
        }
        if (miningTarget > MAXIMUM_TARGET) {
            miningTarget = MAXIMUM_TARGET;
        }

        latestDifficultyPeriodStarted = block.number;

        emit DifficultyAdjusted(
            oldTarget,
            miningTarget,
            ethBlocksSinceLastPeriod
        );
    }

    // ========================================================================
    // Linear Vesting Mining Functions
    // ========================================================================

    function submitHashPower(uint256 nonce) external returns (bool success) {
        return _submitHashPower(nonce, msg.sender);
    }

    function submitHashPowerFor(
        uint256 nonce,
        address miner
    ) external returns (bool success) {
        return _submitHashPower(nonce, miner);
    }

    function _submitHashPower(
        uint256 nonce,
        address miner
    ) internal returns (bool) {
        if (miner == address(0)) revert ZeroAddress();

        bytes32 digest = keccak256(
            abi.encodePacked(
                keccak256(abi.encodePacked(challengeNumber, miner, nonce))
            )
        );

        if (uint256(digest) > miningTarget) revert InvalidProofOfWork();
        if (lastRewardEthBlockNumber == block.number)
            revert AlreadyMinedInBlock();

        MinerStake storage stake = minerStakes[miner];

        if (stake.firstSubmissionTime == 0) {
            stake.firstSubmissionTime = block.timestamp;
            stake.lastClaimTime = block.timestamp;
            unchecked {
                totalActiveMiners++;
            }
        }

        unchecked {
            stake.accumulatedHashPower++;
            stake.totalHashPowerSubmitted++;
        }

        lastRewardEthBlockNumber = block.number;
        challengeNumber = blockhash(block.number - 1);

        emit HashPowerSubmitted(miner, 1, stake.accumulatedHashPower);

        return true;
    }

    function claimVestingReward() external returns (uint256 amount) {
        MinerStake storage stake = minerStakes[msg.sender];

        if (stake.firstSubmissionTime == 0) revert InsufficientHashPower();

        uint256 timeSinceLastClaim;
        unchecked {
            timeSinceLastClaim = block.timestamp - stake.lastClaimTime;
        }

        if (timeSinceLastClaim < 1 days) revert TooSoonToClaim();

        uint256 daysPassed = timeSinceLastClaim / 1 days;
        uint256 baseReward = daysPassed * DAILY_VESTING_REWARD;
        uint256 requiredHashPower = daysPassed * REQUIRED_DAILY_HASH_POWER;

        uint256 actualReward = baseReward;
        if (stake.accumulatedHashPower < requiredHashPower) {
            actualReward =
                (baseReward * stake.accumulatedHashPower) /
                requiredHashPower;
        }

        if (totalVestingClaimed + actualReward > VESTING_POOL_ALLOCATION) {
            revert VestingPoolDepleted();
        }

        _mint(msg.sender, actualReward);

        unchecked {
            totalVestingClaimed += actualReward;
            stake.totalVestingClaimed += actualReward;
        }
        stake.lastClaimTime = block.timestamp;
        stake.accumulatedHashPower = 0;

        emit VestingRewardClaimed(
            msg.sender,
            actualReward,
            daysPassed,
            requiredHashPower
        );

        return actualReward;
    }

    // ========================================================================
    // View Functions
    // ========================================================================

    function getChallengeNumber() external view returns (bytes32) {
        return challengeNumber;
    }

    function getMiningTarget() external view returns (uint256) {
        return miningTarget;
    }

    function getMiningDifficulty() external view returns (uint256) {
        return MAXIMUM_TARGET / miningTarget;
    }

    function getMiningReward() external view returns (uint256) {
        return currentMiningReward;
    }

    struct MiningStats {
        uint256 currentReward;
        uint256 difficulty;
        uint256 target;
        uint256 epochCount;
        uint256 rewardEra;
        uint256 tokensMinted;
        uint256 percentMined;
        uint256 blocksUntilAdjustment;
        bytes32 challengeNumber;
        uint256 totalVestingClaimed;
        uint256 vestingPoolRemaining;
        uint256 totalActiveMiners;
    }

    function getMiningStats() external view returns (MiningStats memory stats) {
        stats.currentReward = currentMiningReward;
        stats.difficulty = MAXIMUM_TARGET / miningTarget;
        stats.target = miningTarget;
        stats.epochCount = epochCount;
        stats.rewardEra = rewardEra;
        stats.tokensMinted = tokensMinted;
        stats.percentMined = (tokensMinted * 100) / POW_ALLOCATION;
        stats.blocksUntilAdjustment =
            BLOCKS_PER_READJUSTMENT -
            (epochCount % BLOCKS_PER_READJUSTMENT);
        stats.challengeNumber = challengeNumber;
        stats.totalVestingClaimed = totalVestingClaimed;
        stats.vestingPoolRemaining =
            VESTING_POOL_ALLOCATION -
            totalVestingClaimed;
        stats.totalActiveMiners = totalActiveMiners;
    }

    struct MinerVestingStats {
        uint256 accumulatedHashPower;
        uint256 totalVestingClaimed;
        uint256 totalHashPowerSubmitted;
        uint256 pendingReward;
        uint256 daysSinceLastClaim;
        uint256 requiredHashPowerForFullReward;
        uint256 firstSubmissionTime;
        uint256 lastClaimTime;
    }

    function getMinerVestingStats(
        address miner
    ) external view returns (MinerVestingStats memory stats) {
        MinerStake storage stake = minerStakes[miner];

        stats.accumulatedHashPower = stake.accumulatedHashPower;
        stats.totalVestingClaimed = stake.totalVestingClaimed;
        stats.totalHashPowerSubmitted = stake.totalHashPowerSubmitted;
        stats.firstSubmissionTime = stake.firstSubmissionTime;
        stats.lastClaimTime = stake.lastClaimTime;

        if (stake.lastClaimTime > 0) {
            uint256 timeSinceLastClaim = block.timestamp - stake.lastClaimTime;
            if (timeSinceLastClaim >= 1 days) {
                uint256 daysPassed = timeSinceLastClaim / 1 days;
                uint256 baseReward = daysPassed * DAILY_VESTING_REWARD;
                uint256 requiredHashPower = daysPassed *
                    REQUIRED_DAILY_HASH_POWER;

                stats.daysSinceLastClaim = daysPassed;
                stats.requiredHashPowerForFullReward = requiredHashPower;

                if (stake.accumulatedHashPower >= requiredHashPower) {
                    stats.pendingReward = baseReward;
                } else {
                    stats.pendingReward =
                        (baseReward * stake.accumulatedHashPower) /
                        requiredHashPower;
                }
            }
        }
    }

    function checkMintSolution(
        uint256 nonce,
        address miner
    ) external view returns (bool isValid) {
        bytes32 digest = keccak256(
            abi.encodePacked(
                keccak256(abi.encodePacked(challengeNumber, miner, nonce))
            )
        );
        return uint256(digest) <= miningTarget;
    }

    // ========================================================================
    // Security
    // ========================================================================

    receive() external payable {
        revert ETHNotAccepted();
    }

    fallback() external payable {
        revert ETHNotAccepted();
    }
}
"
    }
  },
  "settings": {
    "optimizer": {
      "enabled": true,
      "runs": 200,
      "details": {
        "yul": true,
        "yulDetails": {
          "stackAllocation": true,
          "optimizerSteps": "dhfoDgvulfnTUtnIf"
        }
      }
    },
    "viaIR": false,
    "evmVersion": "paris",
    "outputSelection": {
      "*": {
        "*": [
          "evm.bytecode",
          "evm.deployedBytecode",
          "abi"
        ]
      }
    }
  }
}}

Tags:
ERC20, DeFi, Mintable, Factory|addr:0xb78ecc5a33fe5ba907e37ed60c86d1621e59e20a|verified:true|block:23590167|tx:0xac53f09f7c776b2e84dd79858e4c6997321e9f10b8fb334c8d2efa58c7a8bfc4|first_check:1760620635

Submitted on: 2025-10-16 15:17:18

Comments

Log in to comment.

No comments yet.