Skip to content

Public trust

Fenrua FEN Token Public Trust Statement

This page is the clear public holder surface for FEN, P-FEN, audit evidence, SHA references, and the future N burn-to-mint rule. If a promise is made here, it should be visible through contract code, vault balances, dashboard records, live block progress, or a published receipt hash.

Version

0.4

Report date

2026-07-08

Scope

FEN launch

FENpresale deadline

360 days

Fixed deadline counted from the presale start day; early sellout can finalize sooner.

Scoped verdict

Pass for the scoped launch-accountability bundle.

This verdict covers the launch-accountability bundle and public statement evidence. It is not a blanket approval for future launches, price outcomes, FENpool returns, or protected controls.

Abstract 1. Appendix Abstract

FENtrust is the source-backed proof appendix for the public protocol.

This section is the public evidence appendix for Fenrua Nexus. It does not ask the reader to infer trust from branding or tone. Instead, it places holder commitments, report hashes, source manifests, contract source, curated excerpts, full source panels, and the trust statement itself into one inspectable record. The section also states what this evidence cannot prove, because proof without scope is a misleading artifact.

Evidence rule: a public promise is admissible only when its artifact, scope, and limitation are visible together.

Definitions

Definition 1.Scoped verdict
A pass/fail statement limited to the named bundle, report, artifact set, and tests that produced the result.
Definition 2.Source excerpt
A curated portion of contract code selected to prove a public mechanism claim while preserving a link to full source.
Definition 3.Evidence hash
A SHA256 reference used to identify a public artifact, not a standalone proof that all possible claims are true.
Definition 4.Holder commitment
A user-facing mechanism statement that must remain comparable with source code, reports, receipts, and public chain data.

Research Questions

Research Question 1. RQ-T-01

What converts a public claim into evidence-backed text?

A claim becomes admissible only when it points to source code, source manifests, report hashes, audit outcomes, live chain progress, or a named boundary condition.

Research Question 2. RQ-T-02

How does FENtrust prevent proof laundering?

It separates hash identity, source excerpts, full source panels, scoped verdicts, and non-claims so one kind of evidence cannot silently stand in for another.

Research Question 3. RQ-T-03

What should a holder-facing trust statement not do?

It should not promise price, return, liquidity, listing, live controls, external audit coverage beyond scope, or protection from all future protocol risk.

Formal Claim Register

ClaimStatementEvidenceBoundary
Formal Claim 1. FC-T-01FENtrust is the evidence appendix for FEN public claims.The section renders source indexes, source snippets, full contract panels, public trust statement source, report hashes, and audit summary rows.An appendix proves only the artifacts it names and the scope those artifacts claim.
Formal Claim 2. FC-T-02The public trust statement is source-comparable.The statement source is rendered beside SHA references, contract source records, and PFENPresale excerpts.Statement text cannot override contract behavior or expand audit scope beyond published artifacts.
Formal Claim 3. FC-T-03FEN holder commitments are mechanism commitments.Commitments name fixed supply, 0% transfer tax, P-FEN non-transferability, reserve requirement, claim schedule, variable rewards, and N migration constraint.Mechanism commitments are not market-performance, investment, APY, or liquidity guarantees.

Invariant Ledger

Invariant 1. INV-T-01

Hash identity is not the same as unlimited verification.

Hash rows name artifact references while limitation language keeps report scope explicit.

Invariant 2. INV-T-02

Source excerpts must remain connected to full source.

Curated snippets are followed by expandable full contract source panels.

Invariant 3. INV-T-03

Trust claims remain mechanism-specific.

Commitments are phrased around token mechanics, reserve requirements, schedule behavior, and eligibility constraints.

Evidence Matrix

RowArtifactObservationLimitation
Evidence Row 1. T-01docs/security/fen-token-public-trust-statement.mdRenders the public trust statement source directly inside the section.A public statement is bounded by its referenced contracts and reports.
Evidence Row 2. T-02reports/fen-launch-accountability/FINAL-REPORT-v0.4.mdIdentified by public launch-accountability report hash and scoped verdict.The scoped verdict is not a blanket approval for future launches or protected controls.
Evidence Row 3. T-03contracts/presale/*.solFull source panels expose the production trust source set for inspection.Source availability does not replace runtime verification of deployed addresses or balances.

Limitations And Non-Claims

Limitation 1. L-T-01

No market outcome is proven

FENtrust proves mechanism evidence and artifact identity. It does not prove price performance, liquidity, listing, yield, or user profit.

Limitation 2. L-T-02

No future audit scope is implied

A scoped report cannot be stretched to future contracts, future deployments, or future operational changes unless those artifacts are separately reviewed.

Limitation 3. L-T-03

No private operations are exposed

Source and evidence panels remain public proof surfaces; reserve controls, validator operations, private keys, and protected execution remain absent.

proof-center mapping

Claim to source to audit artifact to UI surface.

FENtrust is the proof center when public claims need to stay comparable with source excerpts, audit artifacts, and the page where the claim appears.

ClaimSource excerptAudit artifactUI surface
P-FEN is non-transferable.PFENPresale transfer and approval overrides.Presale source review and launch-accountability bundle.FENpresale / FENswap
P-FEN burns to claim FEN.PFENPresale claim() and claimable() schedule logic.FENpresale audit evidence and public trust statement.FENpresale
FENpool excludes P-FEN.Eligibility language for claimed and unlocked FEN only.Trust statement and reward-eligibility section evidence.FENpool
N mint requires burned FEN.Public migration rule and burned-FEN boundary language.N521 research dossier and trust statement references.FENn521

Token mechanism

01

FENswap entry

Public entry gate for FEN access and the FENpresale path.

02

P-FEN receipt

Non-transferable receipt backed by the exact presale FEN reserve.

03

Burn P-FEN to claim FEN

Unlocked FEN is claimed by burning P-FEN on the contract schedule.

04

Future burn FEN to mint N

Future N minting requires burned FEN under published migration rules.

05

Stake unlocked FEN in FENpool

Only unlocked and claimed FEN can enter FENpool rewards.

Holder commitments

FEN is the fixed-supply token on FENchain chain 978 with 1,000,000,000 FEN.

FEN has 0% ERC20 transfer tax and remains burnable by holders.

P-FEN is a presale receipt only: non-transferable, non-refundable, with no approval market and no liquidity pool.

FENpresale cannot open unless exactly 200,000,000 FEN is locked for the presale reserve.

FENpresale has a fixed 360 days deadline from the presale start day; early sellout can finalize sooner.

Unsold presale FEN is burned at finalization.

FEN claims begin 120 days after presale end, then unlock by the published schedule.

FENpool rewards are variable and depend on funded reward epochs. No fixed APY.

P-FEN cannot be staked; locked or unclaimed FEN cannot be staked; only unlocked and claimed FEN can enter FENpool rewards.

Future N minting must require burned FEN under published migration contract rules.

Public dashboard records

The website explains the facts. Contracts, hashes, receipts, and live block progress prove the facts once the relevant source records and receipts are published.

FEN token source reference

FENpresale contract source reference

allocation vault source references

FENpool and rewards vault source references

multisig source references

FENpresale reserve balance

burn totals

receipt hashes

audit report hashes

live block progress

Audit evidence summary

The scoped report records replayable test and review evidence for the launch accountability contracts and public statement work.

Hardhat invariant test: pass.

Foundry test suite: 8 passed; 0 failed; 0 skipped.

Strict launch accountability audit: 18 checks / 0 failures.

Slither scoped contract review: 0 result(s) found.

SHA references

These public hashes give readers stable references for the statement, report, source set, and evidence set used by the launch-accountability bundle.

Public statement SHA256

6121a9dcbf564db8cfcde7cb35b570c9d6ccc127626ceedb69eb08648750c199

Fenrua FEN Token Public Trust Statement v0.4.

Presale final report

fd65c0bfbedd89b3397b827a103fb65897607b428a5855e618f97888d08a2561

PFENPresale audit and launch-readiness package.

Launch accountability final report

08360fd364a5c3845c25bd2c0837b7bb351f684afa1ec7d5c8b727b4525f70ae

Launch accountability bundle referenced by the public trust statement.

Genesis allocation final report

59c6a2e3591e61bfc1e8620198a71ffab4078063b94156876b8864ff921774d0

Genesis allocation and custody evidence report.

Presale source manifest

73516fb7b25e83b58ba8557152a5eb7a50f70cbad1564ed10029fdce7cafa340

Source manifest for the presale audit bundle.

Launch source manifest

f8e1abaf3f0fce91e43975cbcd864787e1a0574b5e575a4001a4682c8e32230f

Source manifest for launch accountability.

Genesis source manifest

43c8880207901ca4b77d4fe637e83a0958781fd52cbff9105b4d0bb74a3e82d6

Source manifest for genesis allocation.

Evidence manifest

c55d265a3a7c003a1632f9f3083c4a47728b855c54497ed11533bae96a5b6d67

Evidence manifest for the launch-accountability bundle.

Explorer evidence

Fenrua-owned explorer surfaces show read-only chain data for support-chain and research-chain review. Contract address links appear later when deployment, reserve lock, and authority evidence are complete.

Contract source evidence

The public trust surface lists the source set first, then shows the exact P-FEN constructor, purchase, claim, claimable, and non-transferability excerpts.

FenruaFEN

contracts/presale/FenruaFEN.sol

Fixed-supply FEN token, burnable, launch-gated transfer boundary.

Production trust source

PFENPresale

contracts/presale/PFENPresale.sol

P-FEN receipt sale, reserve lock, staged pricing, claim schedule, non-transferability.

Production trust source

FENTransparencyRegistry

contracts/presale/FENTransparencyRegistry.sol

Public receipt and source-status registry for launch accountability.

Production trust source

FENAllocationVault

contracts/presale/FENAllocationVault.sol

Purpose-bound allocation custody and release receipts.

Production trust source

FENStakingPool

contracts/presale/FENStakingPool.sol

FENpool lock plans, funded reward epochs, wallet caps, and reward claims.

Production trust source

FENVestingVault

contracts/presale/FENVestingVault.sol

Contract-enforced vesting and release schedule custody.

Production trust source

MockStablecoin

contracts/mocks/MockStablecoin.sol

Audit and test support source, not a production trust contract.

Audit support source

PFENPresale constructor

contracts/presale/PFENPresale.sol

Locks the sale to FENchain 978, validates FEN/payment/treasury/owner addresses, and builds the exact 200M P-FEN stage allocation.

constructor(
        IPresaleFEN fen_,
        IERC20Metadata paymentToken_,
        address treasury_
    ) ERC20('Presale FEN Receipt', 'P-FEN') {
        if (block.chainid != FENCHAIN_CHAIN_ID) {
            revert WrongFenchain(block.chainid, FENCHAIN_CHAIN_ID);
        }
        if (address(fen_) == address(0) || address(paymentToken_) == address(0)) revert ZeroAddress();
        if (treasury_ == address(0)) revert ZeroAddress();
        address configuredLaunchController = fen_.launchController();
        if (configuredLaunchController != treasury_) {
            revert InvalidLaunchController(configuredLaunchController, treasury_);
        }

        uint8 decimals_ = paymentToken_.decimals();
        if (decimals_ < 2 || decimals_ > 30) revert BadStablecoinDecimals(decimals_);

        fen = fen_;
        paymentToken = IERC20(address(paymentToken_));
        treasury = treasury_;
        paymentTokenDecimals = decimals_;

        uint256 unit = 10 ** uint256(decimals_);
        _stages[0] = Stage({allocation: 2_500_000 ether, sold: 0, price: unit / 100});
        _stages[1] = Stage({allocation: 5_000_000 ether, sold: 0, price: (unit * 2) / 100});
        _stages[2] = Stage({allocation: 7_500_000 ether, sold: 0, price: (unit * 3) / 100});
        for (uint256 i = 3; i < 14; i++) {
            _stages[i] = Stage({allocation: 15_000_000 ether, sold: 0, price: (unit * (i + 1)) / 100});
        }
        _stages[14] = Stage({allocation: 20_000_000 ether, sold: 0, price: (unit * 15) / 100});

        uint256 totalAllocation = 0;
        for (uint256 i = 0; i < STAGE_COUNT; i++) {
            totalAllocation += _stages[i].allocation;
        }
        if (totalAllocation != PRESALE_ALLOCATION) {
            revert ReserveInvariantBroken(totalAllocation, PRESALE_ALLOCATION);
        }
    }

buyPFEN

contracts/presale/PFENPresale.sol

Requires an open, unfinalized sale, exact reserve backing, current-stage capacity, stablecoin payment forwarding, and P-FEN minting.

function buyPFEN(
        uint256 pfenAmount,
        uint256 expectedStage,
        uint256 maxStablecoinAmount,
        uint256 deadline
    ) external nonReentrant {
        if (!opened) revert PresaleNotOpen();
        if (soldOutAt != 0) revert PresaleAlreadyFinalized();
        // slither-disable-next-line timestamp
        if (block.timestamp >= saleEnd) revert PresaleClosed(saleEnd);
        // slither-disable-next-line timestamp
        if (block.timestamp > deadline) revert PurchaseExpired(deadline, block.timestamp);
        if (pfenAmount == 0) revert ZeroAmount();
        _requireActiveReserve();

        uint256 stageIndex = currentStage;
        if (stageIndex != expectedStage) revert StageChanged(expectedStage, stageIndex);

        Stage storage activeStage = _stages[stageIndex];
        uint256 remaining = activeStage.allocation - activeStage.sold;
        if (pfenAmount > remaining) {
            revert StageAllocationExceeded(stageIndex, pfenAmount, remaining);
        }

        uint256 stablecoinAmount = quotePFEN(stageIndex, pfenAmount);
        if (stablecoinAmount > maxStablecoinAmount) {
            revert PaymentExceedsMaximum(stablecoinAmount, maxStablecoinAmount);
        }
        uint256 treasuryBalanceBefore = paymentToken.balanceOf(treasury);
        paymentToken.safeTransferFrom(msg.sender, treasury, stablecoinAmount);
        uint256 treasuryBalanceAfter = paymentToken.balanceOf(treasury);
        uint256 stablecoinReceived =
            treasuryBalanceAfter >= treasuryBalanceBefore ? treasuryBalanceAfter - treasuryBalanceBefore : 0;
        if (stablecoinReceived < stablecoinAmount) {
            revert TreasuryPaymentShortfall(stablecoinAmount, stablecoinReceived);
        }

        activeStage.sold += pfenAmount;
        totalSoldPFEN += pfenAmount;
        totalStablecoinRaised += stablecoinReceived;
        purchasedPFEN[msg.sender] += pfenAmount;

        emit PaymentForwarded(
            msg.sender,
            address(paymentToken),
            stablecoinAmount,
            stablecoinReceived,
            treasury
        );

        _mint(msg.sender, pfenAmount);
        emit PFENPurchased(msg.sender, stageIndex, pfenAmount, stablecoinReceived, treasury);

        _advanceStage();

        if (totalSoldPFEN == PRESALE_ALLOCATION) {
            _recordSellout(block.timestamp);
        }
    }

claim

contracts/presale/PFENPresale.sol

Burns claimable P-FEN and transfers the same amount of unlocked real FEN to the claimant.

function claim() external nonReentrant returns (uint256 amount) {
        amount = claimable(msg.sender);
        // slither-disable-next-line incorrect-equality,timestamp
        if (amount == 0) revert NoClaimableFEN(msg.sender);

        uint256 reserveBeforeClaim = fen.balanceOf(address(this));
        uint256 requiredBeforeClaim = outstandingFEN();
        if (reserveBeforeClaim < requiredBeforeClaim) {
            revert ReserveInvariantBroken(reserveBeforeClaim, requiredBeforeClaim);
        }

        claimedFEN[msg.sender] += amount;
        totalClaimedFEN += amount;
        _burn(msg.sender, amount);
        fen.safeTransfer(msg.sender, amount);

        uint256 reserveAfterClaim = fen.balanceOf(address(this));
        uint256 requiredAfterClaim = outstandingFEN();
        if (reserveAfterClaim < requiredAfterClaim) {
            revert ReserveInvariantBroken(reserveAfterClaim, requiredAfterClaim);
        }
        emit FENClaimed(msg.sender, amount, amount);
    }

claimable

contracts/presale/PFENPresale.sol

Computes cliff-schedule unlocks after finalization and subtracts already claimed FEN.

function claimable(address account) public view returns (uint256) {
        uint256 resolvedClaimStart = claimStart();
        if (resolvedClaimStart == 0 || block.timestamp < resolvedClaimStart) return 0;

        uint256 periods = ((block.timestamp - resolvedClaimStart) / RELEASE_INTERVAL) + 1;
        uint256 unlockedBasisPoints =
            periods >= FINAL_RELEASE_PERIOD ? BPS : periods * RELEASE_BPS;
        uint256 unlocked = (purchasedPFEN[account] * unlockedBasisPoints) / BPS;
        uint256 alreadyClaimed = claimedFEN[account];
        if (unlocked <= alreadyClaimed) return 0;
        return unlocked - alreadyClaimed;
    }

P-FEN non-transferable overrides

contracts/presale/PFENPresale.sol

Rejects approval, direct transfer, transferFrom, and wallet-to-wallet ERC20 movement while still allowing mint and burn.

function approve(address, uint256) public pure override returns (bool) {
        revert PFENNonTransferable();
    }

function transfer(address, uint256) public pure override returns (bool) {
        revert PFENNonTransferable();
    }

function transferFrom(address, address, uint256) public pure override returns (bool) {
        revert PFENNonTransferable();
    }

function _update(address from, address to, uint256 value) internal override {
        if (from != address(0) && to != address(0)) revert PFENNonTransferable();
        super._update(from, to, value);
    }

Full contract source panels

Expand only what you want to inspect; the full source set stays available without turning the page into an unreadable code dump.

FenruaFENcontracts/presale/FenruaFEN.solSource
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

import {ERC20} from '@openzeppelin/contracts/token/ERC20/ERC20.sol';
import {ERC20Burnable} from '@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol';
import {Ownable} from '@openzeppelin/contracts/access/Ownable.sol';

/// @dev Minimal immutable presale binding used to validate claims and launch readiness.
interface IFENPresaleLaunchGate {
    function fen() external view returns (address);
    function treasury() external view returns (address);
    function launchReady() external view returns (bool);
}

/// @notice Fixed-supply FEN for chain 978 with a one-way genesis ceremony and treasury launch gate.
contract FenruaFEN is ERC20, ERC20Burnable, Ownable {
    error ZeroAddress();
    error WrongFenchain(uint256 actualChainId, uint256 requiredChainId);
    error NotContract(address account);
    error PresaleControllerAlreadySet(address controller);
    error InvalidPresaleFEN(address configuredFen);
    error InvalidPresaleTreasury(address configuredTreasury);
    error PresaleControllerNotSet();
    error GenesisBalanceNotAllocated(address distributor, uint256 remainingBalance);
    error GenesisTransferAllowanceStillActive(address distributor);
    error CeremonyOwnerBalanceNotZero(address ceremonyOwner, uint256 remainingBalance);
    error CeremonyOwnerTransferAllowanceActive(address ceremonyOwner);
    error GenesisCeremonyNotSealed();
    error PresaleNotLaunchReady(address controller);
    error NotLaunchController(address caller);
    error PreLaunchTransferAllowanceNotSet(address account);
    error TransferLockedBeforeLaunch(address from, address to);

    uint256 public constant FENCHAIN_CHAIN_ID = 978;
    uint256 public constant GENESIS_SUPPLY = 1_000_000_000 ether;

    address public immutable launchController; // Treasury wallet; cannot be changed.
    address public immutable genesisDistributor; // Temporary ceremony account.
    address public presaleController; // One-time-bound P-FEN claim distributor and launch gate.
    bool public deploymentSealed; // One-way proof that temporary genesis authority exited.
    bool public launched;
    mapping(address account => bool allowed) public preLaunchTransferAllowed;

    event PresaleControllerSet(address indexed controller);
    event GenesisDeploymentSealed(address indexed genesisDistributor);
    event PreLaunchTransferAllowed(address indexed account, bool allowed);
    event FENLaunched(address indexed controller, uint256 launchedAt);

    constructor(address initialOwner, address launchController_)
        ERC20('Fenrua FEN', 'FEN')
        Ownable(initialOwner)
    {
        if (block.chainid != FENCHAIN_CHAIN_ID) {
            revert WrongFenchain(block.chainid, FENCHAIN_CHAIN_ID);
        }
        if (launchController_ == address(0)) revert ZeroAddress();

        launchController = launchController_;
        genesisDistributor = initialOwner;
        preLaunchTransferAllowed[initialOwner] = true;
        _mint(initialOwner, GENESIS_SUPPLY);
        emit PreLaunchTransferAllowed(initialOwner, true);
    }

    /// @notice Binds the production presale once and grants only it pre-launch claim transfers.
    function setPresaleController(address controller) external onlyOwner {
        if (controller == address(0)) revert ZeroAddress();
        if (controller.code.length == 0) revert NotContract(controller);
        if (presaleController != address(0)) {
            revert PresaleControllerAlreadySet(presaleController);
        }

        address configuredFen = IFENPresaleLaunchGate(controller).fen();
        if (configuredFen != address(this)) revert InvalidPresaleFEN(configuredFen);

        address configuredTreasury = IFENPresaleLaunchGate(controller).treasury();
        if (configuredTreasury != launchController) {
            revert InvalidPresaleTreasury(configuredTreasury);
        }

        presaleController = controller;
        preLaunchTransferAllowed[controller] = true;
        emit PresaleControllerSet(controller);
        emit PreLaunchTransferAllowed(controller, true);
    }

    /// @notice Enables ordinary FEN transfers after the treasury and presale gates both approve.
    function activateLaunch() external {
        if (msg.sender != launchController) revert NotLaunchController(msg.sender);
        if (!deploymentSealed) revert GenesisCeremonyNotSealed();
        address controller = presaleController;
        if (controller == address(0)) revert PresaleControllerNotSet();
        if (!IFENPresaleLaunchGate(controller).launchReady()) {
            revert PresaleNotLaunchReady(controller);
        }
        if (!launched) {
            launched = true;
            emit FENLaunched(msg.sender, block.timestamp);
        }
    }

    function ceremonySealed() public view returns (bool) {
        return deploymentSealed;
    }

    /// @notice Seals genesis only after all FEN and transfer authority have left the distributor.
    function renounceOwnership() public override onlyOwner {
        if (presaleController == address(0)) revert PresaleControllerNotSet();

        uint256 distributorBalance = balanceOf(genesisDistributor);
        if (distributorBalance != 0) {
            revert GenesisBalanceNotAllocated(genesisDistributor, distributorBalance);
        }
        if (preLaunchTransferAllowed[genesisDistributor]) {
            revert GenesisTransferAllowanceStillActive(genesisDistributor);
        }

        address ceremonyOwner = owner();
        uint256 ceremonyOwnerBalance = balanceOf(ceremonyOwner);
        if (ceremonyOwnerBalance != 0) {
            revert CeremonyOwnerBalanceNotZero(ceremonyOwner, ceremonyOwnerBalance);
        }
        if (preLaunchTransferAllowed[ceremonyOwner]) {
            revert CeremonyOwnerTransferAllowanceActive(ceremonyOwner);
        }

        deploymentSealed = true;
        emit GenesisDeploymentSealed(genesisDistributor);
        super.renounceOwnership();
    }

    function renouncePreLaunchTransferAllowance() external {
        if (!preLaunchTransferAllowed[msg.sender]) {
            revert PreLaunchTransferAllowanceNotSet(msg.sender);
        }

        preLaunchTransferAllowed[msg.sender] = false;
        emit PreLaunchTransferAllowed(msg.sender, false);
    }

    function _update(address from, address to, uint256 value) internal override {
        if (!launched && from != address(0) && to != address(0)) {
            if (!preLaunchTransferAllowed[from]) {
                revert TransferLockedBeforeLaunch(from, to);
            }
        }
        super._update(from, to, value);
    }
}
PFENPresalecontracts/presale/PFENPresale.solSource
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

import {ERC20} from '@openzeppelin/contracts/token/ERC20/ERC20.sol';
import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol';
import {IERC20Metadata} from '@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol';
import {SafeERC20} from '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol';
import {ReentrancyGuard} from '@openzeppelin/contracts/utils/ReentrancyGuard.sol';

interface IPresaleFEN is IERC20 {
    function burn(uint256 amount) external;
    function launched() external view returns (bool);
    function launchController() external view returns (address);
    function presaleController() external view returns (address);
    function ceremonySealed() external view returns (bool);
}

/// @notice Non-transferable P-FEN receipts backed 1:1 by FEN held under a deterministic claim clock.
contract PFENPresale is ERC20, ReentrancyGuard {
    using SafeERC20 for IERC20;
    using SafeERC20 for IPresaleFEN;

    error ZeroAddress();
    error NotTreasury(address caller);
    error WrongFenchain(uint256 actualChainId, uint256 requiredChainId);
    error ZeroAmount();
    error BadStablecoinDecimals(uint8 decimals_);
    error InvalidLaunchController(address actual, address required);
    error InvalidPresaleController(address actual, address required);
    error GenesisCeremonyNotSealed();
    error FENAlreadyLaunched();
    error PresaleAlreadyOpened();
    error PresaleNotOpen();
    error PresaleAlreadyFinalized();
    error PresaleStillActive(uint256 saleEnd);
    error PresaleClosed(uint256 saleEnd);
    error ReserveNotLocked(uint256 actualReserve, uint256 requiredReserve);
    error TreasuryPaymentShortfall(uint256 requiredAmount, uint256 receivedAmount);
    error PurchaseExpired(uint256 deadline, uint256 currentTimestamp);
    error StageChanged(uint256 expectedStage, uint256 actualStage);
    error PaymentExceedsMaximum(uint256 requiredAmount, uint256 maximumAmount);
    error StageOutOfRange(uint256 stageIndex);
    error StageAllocationExceeded(uint256 stageIndex, uint256 requested, uint256 remaining);
    error PFENNonTransferable();
    error NoClaimableFEN(address account);
    error UnsoldFENAlreadyResolved();
    error ReserveInvariantBroken(uint256 actualReserve, uint256 requiredReserve);

    struct Stage {
        uint256 allocation;
        uint256 sold;
        uint256 price;
    }

    uint256 public constant FENCHAIN_CHAIN_ID = 978;
    uint256 public constant PRESALE_ALLOCATION = 200_000_000 ether;
    uint256 public constant SALE_DURATION = 360 days;
    uint256 public constant CLAIM_DELAY = 120 days;
    uint256 public constant RELEASE_INTERVAL = 30 days;
    uint256 public constant RELEASE_BPS = 300;
    uint256 public constant FINAL_RELEASE_PERIOD = 33;
    uint256 public constant BPS = 10_000;
    uint256 public constant STAGE_COUNT = 15;

    IPresaleFEN public immutable fen;
    IERC20 public immutable paymentToken;
    address public immutable treasury;
    uint8 public immutable paymentTokenDecimals;

    Stage[15] private _stages;

    bool public opened;
    bool public deadlineCheckpointRecorded;
    bool public unsoldFENResolved;
    uint256 public saleStart;
    uint256 public saleEnd;
    uint256 public soldOutAt;
    uint256 public currentStage;
    uint256 public totalSoldPFEN;
    uint256 public totalClaimedFEN;
    uint256 public unsoldFENBurned;
    uint256 public totalStablecoinRaised;

    mapping(address account => uint256 amount) public purchasedPFEN;
    mapping(address account => uint256 amount) public claimedFEN;

    event PresaleOpened(uint256 indexed saleStart, uint256 indexed saleEnd);
    event PFENPurchased(
        address indexed buyer,
        uint256 indexed stageIndex,
        uint256 pfenAmount,
        uint256 stablecoinPaid,
        address indexed treasury
    );
    event PaymentForwarded(
        address indexed buyer,
        address indexed paymentToken,
        uint256 requiredAmount,
        uint256 receivedAmount,
        address indexed treasury
    );
    event UnsoldPFENCapacityCancelled(uint256 amount);
    event UnsoldFENBurned(address indexed treasury, uint256 amount);
    event PresaleFinalized(uint256 indexed presaleEnd, uint256 indexed claimStart, uint256 soldPFEN);
    event DeadlineCheckpointRecorded(
        uint256 indexed presaleEnd,
        uint256 indexed claimStart,
        uint256 soldPFEN,
        uint256 unsoldPFEN
    );
    event FENClaimed(address indexed account, uint256 pfenBurned, uint256 fenClaimed);

    constructor(
        IPresaleFEN fen_,
        IERC20Metadata paymentToken_,
        address treasury_
    ) ERC20('Presale FEN Receipt', 'P-FEN') {
        if (block.chainid != FENCHAIN_CHAIN_ID) {
            revert WrongFenchain(block.chainid, FENCHAIN_CHAIN_ID);
        }
        if (address(fen_) == address(0) || address(paymentToken_) == address(0)) revert ZeroAddress();
        if (treasury_ == address(0)) revert ZeroAddress();
        address configuredLaunchController = fen_.launchController();
        if (configuredLaunchController != treasury_) {
            revert InvalidLaunchController(configuredLaunchController, treasury_);
        }

        uint8 decimals_ = paymentToken_.decimals();
        if (decimals_ < 2 || decimals_ > 30) revert BadStablecoinDecimals(decimals_);

        fen = fen_;
        paymentToken = IERC20(address(paymentToken_));
        treasury = treasury_;
        paymentTokenDecimals = decimals_;

        uint256 unit = 10 ** uint256(decimals_);
        _stages[0] = Stage({allocation: 2_500_000 ether, sold: 0, price: unit / 100});
        _stages[1] = Stage({allocation: 5_000_000 ether, sold: 0, price: (unit * 2) / 100});
        _stages[2] = Stage({allocation: 7_500_000 ether, sold: 0, price: (unit * 3) / 100});
        for (uint256 i = 3; i < 14; i++) {
            _stages[i] = Stage({allocation: 15_000_000 ether, sold: 0, price: (unit * (i + 1)) / 100});
        }
        _stages[14] = Stage({allocation: 20_000_000 ether, sold: 0, price: (unit * 15) / 100});

        uint256 totalAllocation = 0;
        for (uint256 i = 0; i < STAGE_COUNT; i++) {
            totalAllocation += _stages[i].allocation;
        }
        if (totalAllocation != PRESALE_ALLOCATION) {
            revert ReserveInvariantBroken(totalAllocation, PRESALE_ALLOCATION);
        }
    }

    modifier onlyTreasury() {
        if (msg.sender != treasury) revert NotTreasury(msg.sender);
        _;
    }

    /// @notice Opens the sale only after the FEN genesis ceremony is irreversibly sealed.
    function openSale() external onlyTreasury {
        if (opened) revert PresaleAlreadyOpened();
        if (fen.launched()) revert FENAlreadyLaunched();
        if (!fen.ceremonySealed()) revert GenesisCeremonyNotSealed();
        address configuredPresaleController = fen.presaleController();
        if (configuredPresaleController != address(this)) {
            revert InvalidPresaleController(configuredPresaleController, address(this));
        }
        _requireOpeningReserveLocked();

        opened = true;
        saleStart = block.timestamp;
        saleEnd = block.timestamp + SALE_DURATION;
        emit PresaleOpened(saleStart, saleEnd);
    }

    /// @notice Atomically proves the treasury receipt before minting the requested P-FEN.
    /// @param expectedStage Stage authorized by the buyer's quote.
    /// @param maxStablecoinAmount Maximum configured stablecoin debit authorized by the buyer.
    /// @param deadline Last timestamp at which the buyer permits execution.
    function buyPFEN(
        uint256 pfenAmount,
        uint256 expectedStage,
        uint256 maxStablecoinAmount,
        uint256 deadline
    ) external nonReentrant {
        if (!opened) revert PresaleNotOpen();
        if (soldOutAt != 0) revert PresaleAlreadyFinalized();
        // slither-disable-next-line timestamp
        if (block.timestamp >= saleEnd) revert PresaleClosed(saleEnd);
        // slither-disable-next-line timestamp
        if (block.timestamp > deadline) revert PurchaseExpired(deadline, block.timestamp);
        if (pfenAmount == 0) revert ZeroAmount();
        _requireActiveReserve();

        uint256 stageIndex = currentStage;
        if (stageIndex != expectedStage) revert StageChanged(expectedStage, stageIndex);

        Stage storage activeStage = _stages[stageIndex];
        uint256 remaining = activeStage.allocation - activeStage.sold;
        if (pfenAmount > remaining) {
            revert StageAllocationExceeded(stageIndex, pfenAmount, remaining);
        }

        uint256 stablecoinAmount = quotePFEN(stageIndex, pfenAmount);
        if (stablecoinAmount > maxStablecoinAmount) {
            revert PaymentExceedsMaximum(stablecoinAmount, maxStablecoinAmount);
        }
        uint256 treasuryBalanceBefore = paymentToken.balanceOf(treasury);
        paymentToken.safeTransferFrom(msg.sender, treasury, stablecoinAmount);
        uint256 treasuryBalanceAfter = paymentToken.balanceOf(treasury);
        uint256 stablecoinReceived =
            treasuryBalanceAfter >= treasuryBalanceBefore ? treasuryBalanceAfter - treasuryBalanceBefore : 0;
        if (stablecoinReceived < stablecoinAmount) {
            revert TreasuryPaymentShortfall(stablecoinAmount, stablecoinReceived);
        }

        activeStage.sold += pfenAmount;
        totalSoldPFEN += pfenAmount;
        totalStablecoinRaised += stablecoinReceived;
        purchasedPFEN[msg.sender] += pfenAmount;

        emit PaymentForwarded(
            msg.sender,
            address(paymentToken),
            stablecoinAmount,
            stablecoinReceived,
            treasury
        );

        _mint(msg.sender, pfenAmount);
        emit PFENPurchased(msg.sender, stageIndex, pfenAmount, stablecoinReceived, treasury);

        _advanceStage();

        if (totalSoldPFEN == PRESALE_ALLOCATION) {
            _recordSellout(block.timestamp);
        }
    }

    /// @notice Optional indexing checkpoint; claim eligibility does not depend on this transaction.
    function recordDeadlineCheckpoint() external nonReentrant {
        if (!opened) revert PresaleNotOpen();
        if (soldOutAt != 0) revert PresaleAlreadyFinalized();
        if (deadlineCheckpointRecorded) revert PresaleAlreadyFinalized();
        // slither-disable-next-line timestamp
        if (block.timestamp < saleEnd) revert PresaleStillActive(saleEnd);
        deadlineCheckpointRecorded = true;
        uint256 unsoldCapacity = PRESALE_ALLOCATION - totalSoldPFEN;
        emit DeadlineCheckpointRecorded(
            saleEnd,
            saleEnd + CLAIM_DELAY,
            totalSoldPFEN,
            unsoldCapacity
        );
    }

    /// @notice Burns only the computed unsold reserve; it cannot burn holder backing.
    function burnUnsoldFEN() external nonReentrant onlyTreasury returns (uint256 amount) {
        if (!saleEnded()) revert PresaleStillActive(saleEnd);
        if (unsoldFENResolved) revert UnsoldFENAlreadyResolved();

        amount = PRESALE_ALLOCATION - totalSoldPFEN;
        uint256 expectedBeforeBurn = requiredFENReserve();
        uint256 reserveBeforeBurn = fen.balanceOf(address(this));
        if (reserveBeforeBurn < expectedBeforeBurn) {
            revert ReserveInvariantBroken(reserveBeforeBurn, expectedBeforeBurn);
        }

        unsoldFENResolved = true;
        unsoldFENBurned = amount;
        fen.burn(amount);

        uint256 expectedAfterBurn = requiredFENReserve();
        uint256 reserveAfterBurn = fen.balanceOf(address(this));
        if (reserveAfterBurn < expectedAfterBurn) {
            revert ReserveInvariantBroken(reserveAfterBurn, expectedAfterBurn);
        }

        emit UnsoldPFENCapacityCancelled(amount);
        emit UnsoldFENBurned(msg.sender, amount);
    }

    /// @notice Releases vested FEN even when global FEN launch has not occurred.
    function claim() external nonReentrant returns (uint256 amount) {
        amount = claimable(msg.sender);
        // slither-disable-next-line incorrect-equality,timestamp
        if (amount == 0) revert NoClaimableFEN(msg.sender);

        uint256 reserveBeforeClaim = fen.balanceOf(address(this));
        uint256 requiredBeforeClaim = outstandingFEN();
        if (reserveBeforeClaim < requiredBeforeClaim) {
            revert ReserveInvariantBroken(reserveBeforeClaim, requiredBeforeClaim);
        }

        claimedFEN[msg.sender] += amount;
        totalClaimedFEN += amount;
        _burn(msg.sender, amount);
        fen.safeTransfer(msg.sender, amount);

        uint256 reserveAfterClaim = fen.balanceOf(address(this));
        uint256 requiredAfterClaim = outstandingFEN();
        if (reserveAfterClaim < requiredAfterClaim) {
            revert ReserveInvariantBroken(reserveAfterClaim, requiredAfterClaim);
        }
        emit FENClaimed(msg.sender, amount, amount);
    }

    function reserveReady() public view returns (bool) {
        // slither-disable-next-line incorrect-equality
        return fen.balanceOf(address(this)) == PRESALE_ALLOCATION;
    }

    function outstandingFEN() public view returns (uint256) {
        return totalSoldPFEN - totalClaimedFEN;
    }

    function holderReserveReady() public view returns (bool) {
        return fen.balanceOf(address(this)) >= outstandingFEN();
    }

    function requiredFENReserve() public view returns (uint256) {
        uint256 required = outstandingFEN();
        if (!unsoldFENResolved) {
            required += PRESALE_ALLOCATION - totalSoldPFEN;
        }
        return required;
    }

    function reserveInvariantReady() public view returns (bool) {
        return fen.balanceOf(address(this)) >= requiredFENReserve();
    }

    function launchReady() external view returns (bool) {
        return saleEnded() && unsoldFENResolved;
    }

    function finalized() public view returns (bool) {
        return saleEnded();
    }

    /// @notice Time/sellout-derived terminal state requiring no keeper transaction.
    function saleEnded() public view returns (bool) {
        return opened && (soldOutAt != 0 || block.timestamp >= saleEnd);
    }

    function presaleEnd() public view returns (uint256) {
        if (!opened) return 0;
        if (soldOutAt != 0) return soldOutAt;
        if (block.timestamp >= saleEnd) return saleEnd;
        return 0;
    }

    function claimStart() public view returns (uint256) {
        uint256 resolvedPresaleEnd = presaleEnd();
        return resolvedPresaleEnd == 0 ? 0 : resolvedPresaleEnd + CLAIM_DELAY;
    }

    function stage(uint256 stageIndex)
        external
        view
        returns (uint256 allocation, uint256 sold, uint256 price)
    {
        if (stageIndex >= STAGE_COUNT) revert StageOutOfRange(stageIndex);
        Stage memory stageData = _stages[stageIndex];
        return (stageData.allocation, stageData.sold, stageData.price);
    }

    function quotePFEN(uint256 stageIndex, uint256 pfenAmount) public view returns (uint256) {
        if (stageIndex >= STAGE_COUNT) revert StageOutOfRange(stageIndex);
        if (pfenAmount == 0) revert ZeroAmount();
        return _mulDivUp(pfenAmount, _stages[stageIndex].price, 1 ether);
    }

    // slither-disable-start timestamp
    function claimable(address account) public view returns (uint256) {
        uint256 resolvedClaimStart = claimStart();
        if (resolvedClaimStart == 0 || block.timestamp < resolvedClaimStart) return 0;

        uint256 periods = ((block.timestamp - resolvedClaimStart) / RELEASE_INTERVAL) + 1;
        uint256 unlockedBasisPoints =
            periods >= FINAL_RELEASE_PERIOD ? BPS : periods * RELEASE_BPS;
        uint256 unlocked = (purchasedPFEN[account] * unlockedBasisPoints) / BPS;
        uint256 alreadyClaimed = claimedFEN[account];
        if (unlocked <= alreadyClaimed) return 0;
        return unlocked - alreadyClaimed;
    }
    // slither-disable-end timestamp

    // slither-disable-start timestamp
    function unlockedBps() external view returns (uint256) {
        uint256 resolvedClaimStart = claimStart();
        if (resolvedClaimStart == 0 || block.timestamp < resolvedClaimStart) return 0;
        uint256 periods = ((block.timestamp - resolvedClaimStart) / RELEASE_INTERVAL) + 1;
        return periods >= FINAL_RELEASE_PERIOD ? BPS : periods * RELEASE_BPS;
    }
    // slither-disable-end timestamp

    function approve(address, uint256) public pure override returns (bool) {
        revert PFENNonTransferable();
    }

    function transfer(address, uint256) public pure override returns (bool) {
        revert PFENNonTransferable();
    }

    function transferFrom(address, address, uint256) public pure override returns (bool) {
        revert PFENNonTransferable();
    }

    function _recordSellout(uint256 resolvedPresaleEnd) internal {
        _requireActiveReserve();
        soldOutAt = resolvedPresaleEnd;

        unsoldFENResolved = true;

        emit UnsoldPFENCapacityCancelled(0);
        emit PresaleFinalized(resolvedPresaleEnd, resolvedPresaleEnd + CLAIM_DELAY, totalSoldPFEN);
    }

    function _requireOpeningReserveLocked() internal view {
        uint256 reserve = fen.balanceOf(address(this));
        if (reserve != PRESALE_ALLOCATION) {
            revert ReserveNotLocked(reserve, PRESALE_ALLOCATION);
        }
    }

    function _requireActiveReserve() internal view {
        uint256 reserve = fen.balanceOf(address(this));
        if (reserve < PRESALE_ALLOCATION) {
            revert ReserveNotLocked(reserve, PRESALE_ALLOCATION);
        }
    }

    function _advanceStage() internal {
        while (currentStage < STAGE_COUNT && _stages[currentStage].sold == _stages[currentStage].allocation) {
            currentStage++;
        }
    }

    function _update(address from, address to, uint256 value) internal override {
        if (from != address(0) && to != address(0)) revert PFENNonTransferable();
        super._update(from, to, value);
    }

    function _mulDivUp(uint256 a, uint256 b, uint256 denominator) internal pure returns (uint256) {
        uint256 product = a * b;
        return ((product - 1) / denominator) + 1;
    }
}
FENTransparencyRegistrycontracts/presale/FENTransparencyRegistry.solSource
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

import {Ownable} from '@openzeppelin/contracts/access/Ownable.sol';

contract FENTransparencyRegistry is Ownable {
    error WrongFenchain(uint256 actualChainId, uint256 requiredChainId);
    error ZeroAddress();
    error GenesisRecorderIsFinalAuthority();
    error EmptyKey();
    error EmptyReceiptHash();
    error CoreRecordAlreadySet(bytes32 key);
    error CoreRecordOutOfRange(uint256 index);
    error ReceiptAlreadyRecorded(bytes32 receiptKey);
    error ReceiptOutOfRange(uint256 receiptId);
    error WrongFinalAuthority(address proposedAuthority, address requiredAuthority);
    error AuthorityAlreadyFinalized();
    error OwnershipRenunciationForbidden();

    struct CoreRecord {
        address account;
        bytes32 receiptHash;
        string uri;
        uint256 recordedAt;
    }

    struct ReceiptRecord {
        bytes32 category;
        address target;
        uint256 amount;
        bytes32 receiptHash;
        string uri;
        uint256 recordedAt;
    }

    uint256 public constant FENCHAIN_CHAIN_ID = 978;

    address public immutable finalAuthority;
    bool public authorityFinalized;

    mapping(bytes32 key => CoreRecord record) private _coreRecords;
    mapping(bytes32 key => bool set) public coreRecordSet;
    bytes32[] private _coreKeys;

    mapping(bytes32 receiptKey => bool set) public receiptKeySet;
    ReceiptRecord[] private _receipts;

    event CoreRecordSet(
        bytes32 indexed key,
        address indexed account,
        bytes32 indexed receiptHash,
        string uri,
        uint256 recordedAt
    );
    event ReceiptRecorded(
        uint256 indexed receiptId,
        bytes32 indexed category,
        address indexed target,
        uint256 amount,
        bytes32 receiptHash,
        string uri,
        uint256 recordedAt
    );

    constructor(address genesisRecorder_, address finalAuthority_) Ownable(genesisRecorder_) {
        if (block.chainid != FENCHAIN_CHAIN_ID) {
            revert WrongFenchain(block.chainid, FENCHAIN_CHAIN_ID);
        }
        if (finalAuthority_ == address(0)) revert ZeroAddress();
        if (genesisRecorder_ == finalAuthority_) revert GenesisRecorderIsFinalAuthority();

        finalAuthority = finalAuthority_;
    }

    /// @notice Hands the registry to its immutable production authority exactly once.
    /// @dev The temporary genesis recorder cannot redirect ownership, and the final authority
    ///      cannot later transfer this registry to a different account.
    function transferOwnership(address newOwner) public override onlyOwner {
        if (authorityFinalized) revert AuthorityAlreadyFinalized();
        if (newOwner != finalAuthority) revert WrongFinalAuthority(newOwner, finalAuthority);

        authorityFinalized = true;
        super.transferOwnership(newOwner);
    }

    /// @notice Registry control must always remain with the immutable final authority.
    function renounceOwnership() public pure override {
        revert OwnershipRenunciationForbidden();
    }

    function setCoreRecord(
        bytes32 key,
        address account,
        bytes32 receiptHash,
        string calldata uri
    ) external onlyOwner {
        if (key == bytes32(0)) revert EmptyKey();
        if (account == address(0)) revert ZeroAddress();
        if (receiptHash == bytes32(0)) revert EmptyReceiptHash();
        if (coreRecordSet[key]) revert CoreRecordAlreadySet(key);

        uint256 recordedAt = block.timestamp;
        _coreRecords[key] =
            CoreRecord({account: account, receiptHash: receiptHash, uri: uri, recordedAt: recordedAt});
        coreRecordSet[key] = true;
        _coreKeys.push(key);
        emit CoreRecordSet(key, account, receiptHash, uri, recordedAt);
    }

    function recordReceipt(
        bytes32 category,
        address target,
        uint256 amount,
        bytes32 receiptHash,
        string calldata uri
    ) external onlyOwner returns (uint256 receiptId) {
        if (category == bytes32(0)) revert EmptyKey();
        if (receiptHash == bytes32(0)) revert EmptyReceiptHash();

        bytes32 key = receiptKey(category, target, amount, receiptHash);
        if (receiptKeySet[key]) revert ReceiptAlreadyRecorded(key);

        receiptId = _receipts.length;
        uint256 recordedAt = block.timestamp;
        receiptKeySet[key] = true;
        _receipts.push(
            ReceiptRecord({
                category: category,
                target: target,
                amount: amount,
                receiptHash: receiptHash,
                uri: uri,
                recordedAt: recordedAt
            })
        );
        emit ReceiptRecorded(receiptId, category, target, amount, receiptHash, uri, recordedAt);
    }

    /// @notice Computes the identity used to reject a duplicate receipt tuple.
    /// @dev The URI is deliberately excluded: changing presentation metadata must not make
    ///      the same category/target/amount/evidence tuple recordable twice.
    function receiptKey(
        bytes32 category,
        address target,
        uint256 amount,
        bytes32 receiptHash
    ) public pure returns (bytes32) {
        return keccak256(abi.encode(category, target, amount, receiptHash));
    }

    function coreRecord(bytes32 key) external view returns (CoreRecord memory record) {
        return _coreRecords[key];
    }

    function coreRecordCount() external view returns (uint256) {
        return _coreKeys.length;
    }

    function coreRecordKey(uint256 index) external view returns (bytes32 key) {
        if (index >= _coreKeys.length) revert CoreRecordOutOfRange(index);
        return _coreKeys[index];
    }

    function receiptCount() external view returns (uint256) {
        return _receipts.length;
    }

    function receipt(uint256 receiptId) external view returns (ReceiptRecord memory record) {
        if (receiptId >= _receipts.length) revert ReceiptOutOfRange(receiptId);
        return _receipts[receiptId];
    }
}
FENAllocationVaultcontracts/presale/FENAllocationVault.solSource
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol';
import {SafeERC20} from '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol';
import {Ownable} from '@openzeppelin/contracts/access/Ownable.sol';
import {ReentrancyGuard} from '@openzeppelin/contracts/utils/ReentrancyGuard.sol';

contract FENAllocationVault is Ownable, ReentrancyGuard {
    using SafeERC20 for IERC20;

    error WrongFenchain(uint256 actualChainId, uint256 requiredChainId);
    error ZeroAddress();
    error ZeroAmount();
    error EmptyPurpose();
    error EmptyReceiptHash();
    error PurposeNotAllowed(bytes32 purpose);
    error DestinationNotApproved(address destination);
    error SelfDestinationForbidden();
    error AllocationCapExceeded(uint256 requestedReleased, uint256 allocationCap);
    error FENReleaseBalanceMismatch(uint256 balanceBefore, uint256 balanceAfter, uint256 expectedDecrease);
    error FENRescueForbidden();
    error AuthorityImmutable();

    uint256 public constant FENCHAIN_CHAIN_ID = 978;

    IERC20 public immutable fen;
    bytes32 public immutable vaultPurpose;
    uint256 public immutable allocationCap;
    string public vaultName;

    uint256 public releasedFEN;

    mapping(bytes32 purpose => bool allowed) public allowedPurpose;
    mapping(bytes32 purpose => mapping(address destination => bool approved)) public approvedDestinationForPurpose;

    event PurposeAllowed(bytes32 indexed purpose, bool allowed, bytes32 indexed receiptHash, string uri);
    event DestinationApproved(
        address indexed destination,
        bool approved,
        bytes32 indexed purpose,
        bytes32 receiptHash,
        string uri
    );
    event FENReleased(
        address indexed destination,
        uint256 amount,
        bytes32 indexed purpose,
        bytes32 indexed receiptHash,
        string uri
    );
    event NonFENRescued(
        address indexed token,
        address indexed destination,
        uint256 amount,
        bytes32 indexed receiptHash,
        string uri
    );

    constructor(
        IERC20 fen_,
        address owner_,
        string memory vaultName_,
        bytes32 vaultPurpose_,
        uint256 allocationCap_
    ) Ownable(owner_) {
        if (block.chainid != FENCHAIN_CHAIN_ID) {
            revert WrongFenchain(block.chainid, FENCHAIN_CHAIN_ID);
        }
        if (address(fen_) == address(0)) revert ZeroAddress();
        if (vaultPurpose_ == bytes32(0)) revert EmptyPurpose();
        if (allocationCap_ == 0) revert ZeroAmount();

        fen = fen_;
        vaultName = vaultName_;
        vaultPurpose = vaultPurpose_;
        allocationCap = allocationCap_;
    }

    /// @notice Department authority is permanently bound to the genesis multisig.
    function transferOwnership(address) public view override onlyOwner {
        revert AuthorityImmutable();
    }

    /// @notice Department authority cannot be abandoned after genesis.
    function renounceOwnership() public view override onlyOwner {
        revert AuthorityImmutable();
    }

    function allowPurpose(
        bytes32 purpose,
        bool allowed,
        bytes32 receiptHash,
        string calldata uri
    ) external onlyOwner {
        if (purpose == bytes32(0)) revert EmptyPurpose();
        if (receiptHash == bytes32(0)) revert EmptyReceiptHash();

        allowedPurpose[purpose] = allowed;
        emit PurposeAllowed(purpose, allowed, receiptHash, uri);
    }

    function approveDestination(
        address destination,
        bool approved,
        bytes32 purpose,
        bytes32 receiptHash,
        string calldata uri
    ) external onlyOwner {
        if (destination == address(0)) revert ZeroAddress();
        if (destination == address(this)) revert SelfDestinationForbidden();
        if (approved && !allowedPurpose[purpose]) revert PurposeNotAllowed(purpose);
        if (receiptHash == bytes32(0)) revert EmptyReceiptHash();

        approvedDestinationForPurpose[purpose][destination] = approved;
        emit DestinationApproved(destination, approved, purpose, receiptHash, uri);
    }

    function releaseFEN(
        address destination,
        uint256 amount,
        bytes32 purpose,
        bytes32 receiptHash,
        string calldata uri
    ) external nonReentrant onlyOwner {
        if (destination == address(0)) revert ZeroAddress();
        if (destination == address(this)) revert SelfDestinationForbidden();
        if (amount == 0) revert ZeroAmount();
        if (!allowedPurpose[purpose]) revert PurposeNotAllowed(purpose);
        if (!approvedDestinationForPurpose[purpose][destination]) revert DestinationNotApproved(destination);
        if (receiptHash == bytes32(0)) revert EmptyReceiptHash();

        if (amount > allocationCap - releasedFEN) {
            uint256 requestedReleased;
            unchecked {
                requestedReleased = releasedFEN + amount;
            }
            if (requestedReleased < releasedFEN) requestedReleased = type(uint256).max;
            revert AllocationCapExceeded(requestedReleased, allocationCap);
        }
        uint256 nextReleased = releasedFEN + amount;

        releasedFEN = nextReleased;
        uint256 balanceBefore = fen.balanceOf(address(this));
        fen.safeTransfer(destination, amount);
        uint256 balanceAfter = fen.balanceOf(address(this));
        if (balanceAfter > balanceBefore || balanceBefore - balanceAfter != amount) {
            revert FENReleaseBalanceMismatch(balanceBefore, balanceAfter, amount);
        }
        emit FENReleased(destination, amount, purpose, receiptHash, uri);
    }

    function rescueNonFEN(
        IERC20 token,
        address destination,
        uint256 amount,
        bytes32 receiptHash,
        string calldata uri
    ) external nonReentrant onlyOwner {
        if (address(token) == address(0) || destination == address(0)) revert ZeroAddress();
        if (destination == address(this)) revert SelfDestinationForbidden();
        if (address(token) == address(fen)) revert FENRescueForbidden();
        if (amount == 0) revert ZeroAmount();
        if (receiptHash == bytes32(0)) revert EmptyReceiptHash();

        token.safeTransfer(destination, amount);
        emit NonFENRescued(address(token), destination, amount, receiptHash, uri);
    }

    function currentFENBalance() external view returns (uint256) {
        return fen.balanceOf(address(this));
    }
}
FENStakingPoolcontracts/presale/FENStakingPool.solSource
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol';
import {SafeERC20} from '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol';
import {Ownable} from '@openzeppelin/contracts/access/Ownable.sol';
import {ReentrancyGuard} from '@openzeppelin/contracts/utils/ReentrancyGuard.sol';

contract FENStakingPool is Ownable, ReentrancyGuard {
    using SafeERC20 for IERC20;

    error WrongFenchain(uint256 actualChainId, uint256 requiredChainId);
    error ZeroAddress();
    error ZeroAmount();
    error EmptyReceiptHash();
    error BadPlan(uint8 planId);
    error WalletStakeCapExceeded(address account, uint256 requestedStake, uint256 walletStakeCap);
    error EpochNotComplete(uint256 epochId, uint256 epochEnd);
    error EpochAlreadyRecorded(uint256 epochId);
    error EpochOutOfOrder(uint256 requestedEpoch, uint256 expectedEpoch);
    error NoEligibleStake(uint256 epochId);
    error PositionOutOfRange(uint256 positionId);
    error NotPositionOwner(address caller, address positionOwner);
    error NothingClaimable(uint256 positionId);
    error StakeLocked(uint256 positionId, uint256 unlockTime);
    error PositionWithdrawn(uint256 positionId);
    error EpochRollInvariant(uint256 requestedEpoch, uint256 lastRolledEpoch);
    error EpochWeightInvariant(uint256 epochId, uint256 availableWeight, uint256 deactivatingWeight);
    error SelfDestinationForbidden();
    error FENRescueForbidden();
    error AuthorityImmutable();

    struct Plan {
        uint256 lockDuration;
        uint256 weightBps;
        bool active;
    }

    struct Position {
        address owner;
        uint8 planId;
        uint256 amount;
        uint256 weight;
        uint256 startTime;
        uint256 activationEpoch;
        uint256 unlockTime;
        uint256 claimedRewards;
        bool withdrawn;
    }

    struct EpochReceipt {
        uint256 rewardAmount;
        uint256 totalEligibleWeightedStake;
        bytes32 receiptHash;
        string uri;
        uint256 recordedAt;
    }

    uint256 public constant FENCHAIN_CHAIN_ID = 978;
    uint256 public constant BPS = 10_000;
    uint256 public constant ACC_REWARD_PRECISION = 1e24;
    uint256 public constant EPOCH_DURATION = 30 days;
    uint256 public constant PLAN_COUNT = 3;

    IERC20 public immutable fen;
    uint256 public immutable epochZeroStart;

    Plan[3] private _plans;
    Position[] private _positions;

    uint256 public walletStakeCap;
    uint256 public totalPrincipalStaked;
    uint256 public totalActiveWeightedStake;
    uint256 public totalRewardsRecorded;
    uint256 public totalRewardsClaimed;
    uint256 public accRewardPerWeightedStake;
    uint256 public lastRolledEpoch;
    uint256 public nextEpochToRecord;

    mapping(address account => uint256 amount) public walletPrincipalStaked;
    mapping(uint256 epochId => uint256 weight) public pendingActivationWeight;
    mapping(uint256 epochId => uint256 weight) public pendingDeactivationWeight;
    mapping(uint256 positionId => uint256 epochId) public positionDeactivationEpoch;
    // The accumulator at an epoch boundary anchors both positions activating at
    // that epoch and positions whose eligibility ended before that epoch.
    mapping(uint256 epochId => uint256 accReward) public activationAccRewardPerWeightedStake;
    mapping(uint256 epochId => bool recorded) public epochRecorded;
    mapping(uint256 epochId => EpochReceipt receipt) private _epochReceipts;

    event Staked(
        uint256 indexed positionId,
        address indexed account,
        uint8 indexed planId,
        uint256 amount,
        uint256 weight,
        uint256 activationEpoch,
        uint256 unlockTime,
        bytes32 receiptHash,
        string uri
    );
    event EpochRewardRecorded(
        uint256 indexed epochId,
        uint256 rewardAmount,
        uint256 totalEligibleWeightedStake,
        bytes32 indexed receiptHash,
        string uri
    );
    event RewardClaimed(uint256 indexed positionId, address indexed account, uint256 amount);
    event PositionDeactivationScheduled(
        uint256 indexed positionId,
        address indexed account,
        uint256 indexed deactivationEpoch,
        uint256 weight
    );
    event StakeWithdrawn(uint256 indexed positionId, address indexed account, uint256 amount);
    event WalletStakeCapUpdated(uint256 oldCap, uint256 newCap, bytes32 indexed receiptHash, string uri);
    event NonFENRescued(
        address indexed token,
        address indexed destination,
        uint256 amount,
        bytes32 indexed receiptHash,
        string uri
    );

    constructor(IERC20 fen_, address owner_, uint256 walletStakeCap_) Ownable(owner_) {
        if (block.chainid != FENCHAIN_CHAIN_ID) {
            revert WrongFenchain(block.chainid, FENCHAIN_CHAIN_ID);
        }
        if (address(fen_) == address(0)) revert ZeroAddress();
        if (walletStakeCap_ == 0) revert ZeroAmount();

        fen = fen_;
        walletStakeCap = walletStakeCap_;
        epochZeroStart = block.timestamp;

        _plans[0] = Plan({lockDuration: 180 days, weightBps: 10_000, active: true});
        _plans[1] = Plan({lockDuration: 360 days, weightBps: 12_500, active: true});
        _plans[2] = Plan({lockDuration: 540 days, weightBps: 15_000, active: true});
    }

    /// @notice Pool authority is permanently bound to the genesis multisig.
    function transferOwnership(address) public view override onlyOwner {
        revert AuthorityImmutable();
    }

    /// @notice Pool authority cannot be abandoned after genesis.
    function renounceOwnership() public view override onlyOwner {
        revert AuthorityImmutable();
    }

    function stake(
        uint8 planId,
        uint256 amount,
        bytes32 receiptHash,
        string calldata uri
    ) external nonReentrant returns (uint256 positionId) {
        Plan memory selectedPlan = _plan(planId);
        if (amount == 0) revert ZeroAmount();
        if (receiptHash == bytes32(0)) revert EmptyReceiptHash();

        uint256 nextWalletStake = walletPrincipalStaked[msg.sender] + amount;
        if (nextWalletStake > walletStakeCap) {
            revert WalletStakeCapExceeded(msg.sender, nextWalletStake, walletStakeCap);
        }

        uint256 weight = (amount * selectedPlan.weightBps) / BPS;
        uint256 activationEpoch = currentEpoch() + 1;
        uint256 unlockTime = block.timestamp + selectedPlan.lockDuration;

        walletPrincipalStaked[msg.sender] = nextWalletStake;
        totalPrincipalStaked += amount;
        pendingActivationWeight[activationEpoch] += weight;

        positionId = _positions.length;
        _positions.push(
            Position({
                owner: msg.sender,
                planId: planId,
                amount: amount,
                weight: weight,
                startTime: block.timestamp,
                activationEpoch: activationEpoch,
                unlockTime: unlockTime,
                claimedRewards: 0,
                withdrawn: false
            })
        );

        fen.safeTransferFrom(msg.sender, address(this), amount);
        emit Staked(positionId, msg.sender, planId, amount, weight, activationEpoch, unlockTime, receiptHash, uri);
    }

    function recordEpochReward(
        uint256 epochId,
        uint256 rewardAmount,
        bytes32 receiptHash,
        string calldata uri
    ) external nonReentrant onlyOwner {
        if (receiptHash == bytes32(0)) revert EmptyReceiptHash();
        if (epochRecorded[epochId]) revert EpochAlreadyRecorded(epochId);
        if (epochId != nextEpochToRecord) revert EpochOutOfOrder(epochId, nextEpochToRecord);

        uint256 epochEnd_ = epochEnd(epochId);
        if (block.timestamp < epochEnd_) revert EpochNotComplete(epochId, epochEnd_);

        _rollToEpoch(epochId);
        if (rewardAmount > 0) {
            if (totalActiveWeightedStake == 0) revert NoEligibleStake(epochId);
            totalRewardsRecorded += rewardAmount;
            accRewardPerWeightedStake += (rewardAmount * ACC_REWARD_PRECISION) / totalActiveWeightedStake;
            fen.safeTransferFrom(msg.sender, address(this), rewardAmount);
        }

        epochRecorded[epochId] = true;
        nextEpochToRecord = epochId + 1;
        _epochReceipts[epochId] = EpochReceipt({
            rewardAmount: rewardAmount,
            totalEligibleWeightedStake: totalActiveWeightedStake,
            receiptHash: receiptHash,
            uri: uri,
            recordedAt: block.timestamp
        });
        emit EpochRewardRecorded(epochId, rewardAmount, totalActiveWeightedStake, receiptHash, uri);
    }

    function claimReward(uint256 positionId) external nonReentrant returns (uint256 amount) {
        Position storage stakedPosition = _position(positionId);
        _requirePositionOwner(stakedPosition);

        amount = _claimable(positionId, stakedPosition);
        if (amount == 0) revert NothingClaimable(positionId);

        stakedPosition.claimedRewards += amount;
        totalRewardsClaimed += amount;
        fen.safeTransfer(stakedPosition.owner, amount);
        emit RewardClaimed(positionId, stakedPosition.owner, amount);
    }

    function withdraw(uint256 positionId) external nonReentrant returns (uint256 principal, uint256 reward) {
        Position storage stakedPosition = _position(positionId);
        _requirePositionOwner(stakedPosition);
        if (stakedPosition.withdrawn) revert PositionWithdrawn(positionId);
        if (block.timestamp < stakedPosition.unlockTime) revert StakeLocked(positionId, stakedPosition.unlockTime);

        reward = _claimable(positionId, stakedPosition);
        if (reward > 0) {
            stakedPosition.claimedRewards += reward;
            totalRewardsClaimed += reward;
            fen.safeTransfer(stakedPosition.owner, reward);
            emit RewardClaimed(positionId, stakedPosition.owner, reward);
        }

        principal = stakedPosition.amount;
        stakedPosition.withdrawn = true;
        walletPrincipalStaked[stakedPosition.owner] -= principal;
        totalPrincipalStaked -= principal;

        // Eligibility is epoch based: a position must remain staked through an
        // epoch's end to earn that epoch. Scheduling the weight change at the
        // withdrawal epoch preserves every already-completed epoch even when
        // its reward receipt is recorded later. The max branch also makes the
        // accounting safe if a future plan permits withdrawal before activation.
        uint256 deactivationEpoch = currentEpoch();
        if (deactivationEpoch < stakedPosition.activationEpoch) {
            deactivationEpoch = stakedPosition.activationEpoch;
        }
        positionDeactivationEpoch[positionId] = deactivationEpoch;
        pendingDeactivationWeight[deactivationEpoch] += stakedPosition.weight;
        emit PositionDeactivationScheduled(
            positionId,
            stakedPosition.owner,
            deactivationEpoch,
            stakedPosition.weight
        );

        fen.safeTransfer(stakedPosition.owner, principal);
        emit StakeWithdrawn(positionId, stakedPosition.owner, principal);
    }

    function setWalletStakeCap(
        uint256 newWalletStakeCap,
        bytes32 receiptHash,
        string calldata uri
    ) external onlyOwner {
        if (newWalletStakeCap == 0) revert ZeroAmount();
        if (receiptHash == bytes32(0)) revert EmptyReceiptHash();

        uint256 oldCap = walletStakeCap;
        walletStakeCap = newWalletStakeCap;
        emit WalletStakeCapUpdated(oldCap, newWalletStakeCap, receiptHash, uri);
    }

    function rescueNonFEN(
        IERC20 token,
        address destination,
        uint256 amount,
        bytes32 receiptHash,
        string calldata uri
    ) external nonReentrant onlyOwner {
        if (address(token) == address(0) || destination == address(0)) revert ZeroAddress();
        if (destination == address(this)) revert SelfDestinationForbidden();
        if (address(token) == address(fen)) revert FENRescueForbidden();
        if (amount == 0) revert ZeroAmount();
        if (receiptHash == bytes32(0)) revert EmptyReceiptHash();

        token.safeTransfer(destination, amount);
        emit NonFENRescued(address(token), destination, amount, receiptHash, uri);
    }

    function currentEpoch() public view returns (uint256) {
        if (block.timestamp <= epochZeroStart) return 0;
        return (block.timestamp - epochZeroStart) / EPOCH_DURATION;
    }

    function epochEnd(uint256 epochId) public view returns (uint256) {
        return epochZeroStart + ((epochId + 1) * EPOCH_DURATION);
    }

    function positionCount() external view returns (uint256) {
        return _positions.length;
    }

    function position(uint256 positionId) external view returns (Position memory position_) {
        return _position(positionId);
    }

    function plan(uint8 planId) external view returns (Plan memory plan_) {
        return _plan(planId);
    }

    function epochReceipt(uint256 epochId) external view returns (EpochReceipt memory receipt_) {
        return _epochReceipts[epochId];
    }

    function claimable(uint256 positionId) external view returns (uint256) {
        Position storage position_ = _position(positionId);
        return _claimable(positionId, position_);
    }

    function currentFENBalance() external view returns (uint256) {
        return fen.balanceOf(address(this));
    }

    function _rollToEpoch(uint256 epochId) private {
        if (epochId == 0) return;
        if (lastRolledEpoch != epochId - 1) {
            revert EpochRollInvariant(epochId, lastRolledEpoch);
        }

        lastRolledEpoch = epochId;
        uint256 activatingWeight = pendingActivationWeight[epochId];
        uint256 deactivatingWeight = pendingDeactivationWeight[epochId];
        uint256 availableWeight = totalActiveWeightedStake + activatingWeight;
        if (deactivatingWeight > availableWeight) {
            revert EpochWeightInvariant(epochId, availableWeight, deactivatingWeight);
        }
        totalActiveWeightedStake = availableWeight - deactivatingWeight;
        activationAccRewardPerWeightedStake[epochId] = accRewardPerWeightedStake;
    }

    function _claimable(uint256 positionId, Position storage position_) private view returns (uint256) {
        if (position_.activationEpoch > lastRolledEpoch) return 0;

        uint256 activationAcc = activationAccRewardPerWeightedStake[position_.activationEpoch];
        uint256 terminalAcc = accRewardPerWeightedStake;
        uint256 deactivationEpoch = positionDeactivationEpoch[positionId];
        if (deactivationEpoch != 0 && deactivationEpoch <= lastRolledEpoch) {
            terminalAcc = activationAccRewardPerWeightedStake[deactivationEpoch];
        }
        if (terminalAcc <= activationAcc) return 0;

        uint256 accumulated = (position_.weight * (terminalAcc - activationAcc)) / ACC_REWARD_PRECISION;
        if (accumulated <= position_.claimedRewards) return 0;
        return accumulated - position_.claimedRewards;
    }

    function _plan(uint8 planId) private view returns (Plan memory selectedPlan) {
        if (planId >= PLAN_COUNT) revert BadPlan(planId);
        selectedPlan = _plans[planId];
        if (!selectedPlan.active) revert BadPlan(planId);
    }

    function _position(uint256 positionId) private view returns (Position storage position_) {
        if (positionId >= _positions.length) revert PositionOutOfRange(positionId);
        return _positions[positionId];
    }

    function _requirePositionOwner(Position storage position_) private view {
        if (msg.sender != position_.owner) revert NotPositionOwner(msg.sender, position_.owner);
    }
}
FENVestingVaultcontracts/presale/FENVestingVault.solSource
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol';
import {SafeERC20} from '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol';
import {Ownable} from '@openzeppelin/contracts/access/Ownable.sol';
import {ReentrancyGuard} from '@openzeppelin/contracts/utils/ReentrancyGuard.sol';

contract FENVestingVault is Ownable, ReentrancyGuard {
    using SafeERC20 for IERC20;

    error WrongFenchain(uint256 actualChainId, uint256 requiredChainId);
    error ZeroAddress();
    error ZeroAmount();
    error BadSchedule();
    error EmptyReceiptHash();
    error GrantOutOfRange(uint256 grantId);
    error NotBeneficiary(address caller, address beneficiary);
    error NothingClaimable(uint256 grantId);
    error AllocationCapExceeded(uint256 requestedGranted, uint256 allocationCap);
    error GrantReserveNotBacked(uint256 actualReserve, uint256 requiredReserve);
    error FENRescueForbidden();

    struct Grant {
        address beneficiary;
        uint256 totalAmount;
        uint256 start;
        uint256 cliff;
        uint256 interval;
        uint256 releaseBps;
        uint256 initialBps;
        uint256 claimed;
        bytes32 receiptHash;
        string uri;
    }

    uint256 public constant FENCHAIN_CHAIN_ID = 978;
    uint256 public constant BPS = 10_000;
    uint256 public constant MIN_CLIFF = 180 days;
    uint256 public constant STANDARD_INTERVAL = 30 days;
    uint256 public constant STANDARD_MONTHLY_BPS = 50;

    IERC20 public immutable fen;
    uint256 public immutable allocationCap;
    string public vaultName;

    uint256 public totalGranted;
    uint256 public totalClaimed;
    Grant[] private _grants;

    event GrantCreated(
        uint256 indexed grantId,
        address indexed beneficiary,
        uint256 totalAmount,
        bytes32 indexed receiptHash,
        string uri
    );
    event GrantClaimed(uint256 indexed grantId, address indexed beneficiary, uint256 amount);
    event NonFENRescued(
        address indexed token,
        address indexed destination,
        uint256 amount,
        bytes32 indexed receiptHash,
        string uri
    );

    constructor(
        IERC20 fen_,
        address owner_,
        string memory vaultName_,
        uint256 allocationCap_
    ) Ownable(owner_) {
        if (block.chainid != FENCHAIN_CHAIN_ID) {
            revert WrongFenchain(block.chainid, FENCHAIN_CHAIN_ID);
        }
        if (address(fen_) == address(0)) revert ZeroAddress();
        if (allocationCap_ == 0) revert ZeroAmount();

        fen = fen_;
        vaultName = vaultName_;
        allocationCap = allocationCap_;
    }

    function createGrant(
        address beneficiary,
        uint256 totalAmount,
        uint256 start,
        uint256 cliff,
        uint256 interval,
        uint256 releaseBps,
        uint256 initialBps,
        bytes32 receiptHash,
        string calldata uri
    ) external onlyOwner returns (uint256 grantId) {
        grantId = _createGrant(
            beneficiary,
            totalAmount,
            start,
            cliff,
            interval,
            releaseBps,
            initialBps,
            receiptHash,
            uri
        );
    }

    function createStandardGrant(
        address beneficiary,
        uint256 totalAmount,
        uint256 start,
        bytes32 receiptHash,
        string calldata uri
    ) external onlyOwner returns (uint256 grantId) {
        return _createGrant(
            beneficiary,
            totalAmount,
            start,
            MIN_CLIFF,
            STANDARD_INTERVAL,
            STANDARD_MONTHLY_BPS,
            0,
            receiptHash,
            uri
        );
    }

    function claim(uint256 grantId) external nonReentrant returns (uint256 amount) {
        Grant storage grant_ = _grant(grantId);
        if (msg.sender != grant_.beneficiary) revert NotBeneficiary(msg.sender, grant_.beneficiary);

        amount = claimable(grantId);
        if (amount == 0) revert NothingClaimable(grantId);

        grant_.claimed += amount;
        totalClaimed += amount;
        fen.safeTransfer(grant_.beneficiary, amount);
        emit GrantClaimed(grantId, grant_.beneficiary, amount);
    }

    function rescueNonFEN(
        IERC20 token,
        address destination,
        uint256 amount,
        bytes32 receiptHash,
        string calldata uri
    ) external onlyOwner nonReentrant {
        if (address(token) == address(0) || destination == address(0)) revert ZeroAddress();
        if (address(token) == address(fen)) revert FENRescueForbidden();
        if (amount == 0) revert ZeroAmount();
        if (receiptHash == bytes32(0)) revert EmptyReceiptHash();

        token.safeTransfer(destination, amount);
        emit NonFENRescued(address(token), destination, amount, receiptHash, uri);
    }

    function grantCount() external view returns (uint256) {
        return _grants.length;
    }

    function grant(uint256 grantId) external view returns (Grant memory grant_) {
        return _grant(grantId);
    }

    function vested(uint256 grantId) public view returns (uint256) {
        Grant storage grant_ = _grant(grantId);
        if (block.timestamp < grant_.start) return 0;

        uint256 vestedBps = grant_.initialBps;
        uint256 cliffEnd = grant_.start + grant_.cliff;
        if (block.timestamp >= cliffEnd) {
            uint256 periods = ((block.timestamp - cliffEnd) / grant_.interval) + 1;
            vestedBps += periods * grant_.releaseBps;
        }

        if (vestedBps > BPS) vestedBps = BPS;
        return (grant_.totalAmount * vestedBps) / BPS;
    }

    function claimable(uint256 grantId) public view returns (uint256) {
        Grant storage grant_ = _grant(grantId);
        uint256 vestedAmount = vested(grantId);
        if (vestedAmount <= grant_.claimed) return 0;
        return vestedAmount - grant_.claimed;
    }

    function currentFENBalance() external view returns (uint256) {
        return fen.balanceOf(address(this));
    }

    function _grant(uint256 grantId) private view returns (Grant storage grant_) {
        if (grantId >= _grants.length) revert GrantOutOfRange(grantId);
        return _grants[grantId];
    }

    function _createGrant(
        address beneficiary,
        uint256 totalAmount,
        uint256 start,
        uint256 cliff,
        uint256 interval,
        uint256 releaseBps,
        uint256 initialBps,
        bytes32 receiptHash,
        string calldata uri
    ) private returns (uint256 grantId) {
        if (beneficiary == address(0)) revert ZeroAddress();
        if (totalAmount == 0) revert ZeroAmount();
        if (receiptHash == bytes32(0)) revert EmptyReceiptHash();
        if (cliff < MIN_CLIFF || interval == 0 || releaseBps == 0 || initialBps > BPS) {
            revert BadSchedule();
        }
        if (releaseBps > BPS || initialBps + releaseBps > BPS) revert BadSchedule();

        uint256 nextGranted = totalGranted + totalAmount;
        if (nextGranted > allocationCap) {
            revert AllocationCapExceeded(nextGranted, allocationCap);
        }
        uint256 requiredReserve = nextGranted - totalClaimed;
        uint256 actualReserve = fen.balanceOf(address(this));
        if (actualReserve < requiredReserve) {
            revert GrantReserveNotBacked(actualReserve, requiredReserve);
        }

        grantId = _grants.length;
        totalGranted = nextGranted;
        _grants.push();

        Grant storage grant_ = _grants[grantId];
        grant_.beneficiary = beneficiary;
        grant_.totalAmount = totalAmount;
        grant_.start = start;
        grant_.cliff = cliff;
        grant_.interval = interval;
        grant_.releaseBps = releaseBps;
        grant_.initialBps = initialBps;
        grant_.receiptHash = receiptHash;
        grant_.uri = uri;

        emit GrantCreated(grantId, beneficiary, totalAmount, receiptHash, uri);
    }
}
MockStablecoincontracts/mocks/MockStablecoin.solSource
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

import {ERC20} from '@openzeppelin/contracts/token/ERC20/ERC20.sol';

contract MockStablecoin is ERC20 {
    uint8 private immutable _customDecimals;

    constructor(string memory name_, string memory symbol_, uint8 decimals_) ERC20(name_, symbol_) {
        _customDecimals = decimals_;
    }

    function decimals() public view override returns (uint8) {
        return _customDecimals;
    }

    function mint(address to, uint256 amount) external {
        _mint(to, amount);
    }
}

Public trust statement source

The statement is rendered directly so readers can compare public promises with contract code, source manifests, report hashes, and live chain progress.

Fenrua FEN Token Public Trust Statement v0.4
# Fenrua FEN Token Public Trust Statement

Version: 0.4
Status: draft public statement and final contract checklist
Scope: FEN token, P-FEN presale, five allocation vaults, staking pool, public registry, one 4-of-6 multisig, and future N mint policy

This statement is written for holders before they buy or stake. It is intentionally plain: if a promise is made here, it must be visible in contract code, vault balances, public dashboard data, explorer links, or a published receipt hash.

## Core Token Commitments

- FEN is deployed on FENchain `chainId 978`.
- FEN total supply is fixed at `1,000,000,000 FEN`.
- No FEN mint function remains available after deployment.
- FEN has `0%` transfer tax.
- FEN is burnable.
- FEN token ownership is renounced after the deployment ceremony is complete.
- The deployer and owner wallet must hold `0 FEN` after allocation transfers are complete.
- Every surviving production admin role and the presale treasury are controlled by one published role-bound 4-of-6 multisig.
- No team-controlled contract should expose a generic unrestricted FEN withdrawal path.
- Every production contract address, multisig address, vault address, and public receipt link is listed on the website dashboard.

## FENgas, FENcont, And Retail-First Market Protection

FEN token transfers have `0%` token transfer tax. FENgas and FENcont are chain and ecosystem protection mechanics, not a hidden ERC20 transfer tax inside the FEN token.

- FENgas is the public FENchain gas-rate signal shown on the dashboard.
- FENcont means the FEN continuity fee.
- FENcont exists to support chain continuity, infrastructure depth, ecosystem resilience, and retail-first market protection.
- Current FENgas and FENcont values, policy notes, and material changes are published on the dashboard or public receipt portal.
- Fenrua/FENchain reserves the right to protect the ecosystem from abusive price manipulation, including whale pump-and-dump behavior, coordinated manipulation, exploit-driven trading, or transaction patterns that threaten retail participants and chain continuity.
- Ecosystem protection may include slowing, rate-limiting, routing, delaying, or applying additional review to very large, abnormal, or suspicious transaction patterns where the chain, router, dashboard, or ecosystem tooling can lawfully and technically enforce it.
- Launch may include anti-bot and anti-sniper protections to protect retail participants during initial liquidity, listing, and trading bootstrap.
- Launch anti-sniper protections may include transaction pacing, cooldowns, rate limits, maximum transaction or wallet limits, fair-access routing, delayed execution for suspicious bot-like patterns, or other clearly disclosed launch-window controls where technically available.
- Launch anti-sniper protections must be published before launch or trading opens, including the active window, purpose, applicable venues or routes, and where holders can check the current status.
- Anti-sniper protections should be bounded to the launch-risk period or to clearly disclosed market-protection conditions, not kept as an unlimited hidden control.
- These protections are designed for retail-first fairness and ecosystem continuity, not for team trading advantage.
- These protections do not create a hidden right for the team to confiscate holder FEN.
- These protections do not change the fixed FEN token supply.
- These protections do not create a FEN token transfer tax.
- Exact abuse-detection thresholds may be protected from full real-time disclosure if publishing them would make manipulation easier, but the existence, purpose, and public policy of FENgas/FENcont protections must be disclosed before users buy.

## Allocation Commitments

| Allocation                             |            Amount | Public control rule                                                                 |
| -------------------------------------- | ----------------: | ----------------------------------------------------------------------------------- |
| Presale backing                        | `200,000,000 FEN` | Locked inside the P-FEN presale contract before any P-FEN sale opens.               |
| Chain reserve                          | `200,000,000 FEN` | Held in its dedicated public chain-reserve vault.                                   |
| Staking, rewards, and community growth | `200,000,000 FEN` | Held in public reward/community vaults with receipts for funding and distributions. |
| Marketing and partnerships             | `150,000,000 FEN` | Held in public multisig/vault custody with release receipts.                        |
| CEX/DEX reserve                        | `150,000,000 FEN` | Held in its own listing/liquidity reserve contract, untouched by team wallets.      |
| Liquidity                              | `100,000,000 FEN` | Used for liquidity setup and LP lock flows with post-launch receipts.               |

The allocation table sums to exactly `1,000,000,000 FEN`.

## P-FEN Presale Commitments

- P-FEN is a receipt only.
- P-FEN is non-transferable.
- P-FEN has no approval market.
- P-FEN has no liquidity pool.
- The presale is non-refundable.
- The sale cannot open unless the presale contract holds exactly `200,000,000 FEN`; while P-FEN minting is active, its reserve cannot fall below that amount.
- After the sale ends, only the 4-of-6 treasury multisig can burn the exact unsold FEN amount; holder backing cannot be burned.
- FEN launch is a separate 4-of-6 multisig execution and is permitted only after the presale allocation is fully resolved as sold or burned.
- FEN claims begin `120 days` after presale end.
- FEN unlocks at `3%` every `30 days`, with the 33rd release completing the final `4%`.
- Holders claim by burning P-FEN and receiving unlocked real FEN.
- There is no admin claim-release function.

## N Mint Policy

Future N minting must be backed by FEN burn.

- N is not mintable through a free admin mint path.
- N is not mintable from an unbacked treasury allocation.
- N is not mintable from a discretionary team wallet.
- N minting must require a verifiable FEN burn or burn receipt.
- The N contract, bridge, or migration contract must enforce the burn-to-mint rule in code before public launch.
- Any future FEN-to-N migration ratio, chain lane, and receipt format must be published before N minting opens.

This statement does not treat N as launched. It records a binding design requirement for the future N contract path: no N without burned FEN.

## Staking And Rewards Commitments

- The staking pool address is published after deployment.
- The rewards vault address is published after deployment.
- The minimum staking lock is `6 months`.
- Staking supports `6 month`, `12 month`, and `18 month` lock plans.
- Staking rewards are variable.
- There is no fixed APY promise.
- There is no fixed-rate staking policy.
- Plan weights may affect each wallet's pro-rata share of a funded reward epoch, but plan weights are not a promised return, not a fixed yield, and not a guarantee that any epoch will be funded.
- Staking rewards are not first-come-first-serve.
- Rewards are distributed pro-rata by epoch across eligible locked stakers according to the contract rules.
- New stakes should begin earning from the next eligible epoch, not by gaming a partially completed epoch.
- Wallet staking caps may be used to support retail-first fairness and reduce whale domination of early reward epochs.
- Monthly reward epochs should be recorded sequentially, including zero-reward months where applicable, so the public record does not skip weak periods.
- Monthly reward funding depends on approved allocations, chain activity, ecosystem activity, market conditions, operational requirements, infrastructure needs, validator/security needs, liquidity needs, audits, compliance, and treasury policy.
- A weak market or low-revenue month may produce low or zero staking rewards.
- The project is not required to put all chain or ecosystem revenue into staking rewards.
- Infrastructure, chain security, validators, audits, compliance, liquidity, and operations may be funded before staking rewards.
- Each monthly staking reward allocation is published with an on-chain funding transaction and public receipt.
- The dashboard shows the amount actually funded into the staking pool, the epoch reward rate, total eligible weighted stake, claimed rewards, and remaining reward vault balances.
- The dashboard does not need to publish total live ecosystem revenue.

## Community And Public Initiative Commitments

Part of the staking, rewards, and community growth reserve may be used for:

- staking rewards;
- validator or chain activity rewards;
- community events;
- public initiatives;
- builder grants;
- ecosystem campaigns;
- education, documentation, and public adoption efforts.

Each material release should have a public purpose label, transaction hash, destination, amount, and receipt hash.

## CEX, DEX, And Liquidity Commitments

- CEX/DEX reserve tokens are held in their own contract or vault.
- Team wallets do not receive the CEX/DEX reserve directly.
- Release of CEX/DEX reserve tokens requires public multisig control and a listing, liquidity, or venue receipt trail.
- Listing receipts are published in the project forum or public evidence portal.
- The human-readable receipt is matched to an on-chain receipt hash where practical.
- LP lock address and LP lock receipt hash are post-launch records, not required at initial FEN deployment.
- Before LP exists, the dashboard should show the LP lock status as pending.
- After LP is created and locked, the lock address, transaction hash, and receipt hash are published.

## Production Custody And Future Vesting Boundary

- The approved genesis suite deploys no `FENVestingVault` and assigns no FEN to a separate team, developer, beneficiary, or ecosystem wallet.
- The entire fixed supply is held by P-FEN plus five fixed department custody vaults; those contracts, not private wallets, are the production custody boundary. Each vault publishes immutable department identity metadata, while every release uses a separately approved and publicly receipted purpose hash under the 4-of-6 multisig.
- A future beneficiary or team vesting design is outside this production scope. It requires a separately specified contract, a fresh independent audit, public allocation approval, and dashboard disclosure before it can receive FEN.
- Historical or research vesting code is not production-approved and must not be displayed as a deployed genesis contract.

## Public Dashboard Commitments

The public dashboard lists and links:

- FEN token address;
- P-FEN presale address;
- FENgas and FENcont public values or policy receipts;
- launch anti-bot and anti-sniper protection status;
- allocation vault addresses;
- staking pool address;
- rewards vault address;
- staking plan terms, wallet cap, and reward epoch receipts;
- chain reserve address;
- CEX/DEX reserve vault address;
- liquidity vault or LP lock status;
- the single production 4-of-6 multisig address;
- token ownership status;
- deployer wallet FEN balance;
- presale reserve balance;
- burn totals;
- monthly staking reward receipts;
- CEX/DEX listing receipts;
- LP lock receipts after launch;
- audit report hashes;
- source and evidence SHA manifests;
- explorer links for direct verification.

The website explains the facts. The contracts, explorer, hashes, and receipts prove the facts.

## Buyer Acknowledgement Language

Before participating in the presale, the user should be shown a clear acknowledgement:

```text
I understand that the P-FEN presale is non-refundable, P-FEN is non-transferable, FEN unlocks by contract schedule, staking rewards are variable with no guaranteed APY, FENgas/FENcont ecosystem protection policies and launch anti-sniper protections may affect very large, suspicious, or bot-like transaction patterns, and future N minting requires burned FEN under the published migration contract rules.
```

## Read Before You Buy

Participants should read this statement, the presale terms, the contract addresses, the audit evidence, and the public receipt dashboard before buying or staking.

No buyer should rely on private messages, informal promises, screenshots, or social media claims when the contract, dashboard, explorer, or receipt record says something different.

Read before buying or entering FENpool.

Buyers should rely on this statement, FENpresale terms, contract source records, audit evidence, public receipts, and public records, not private messages, screenshots, informal promises, or social posts that disagree with the public record.