// SPDX-License-Identifier: MIT pragma solidity ^0.8.24; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title LaunchToken /// @notice A standard, fixed-supply ERC-20 for the Fledge launchpad, with /// an OPTIONAL, IMMUTABLE transfer tax. /// @dev Anti-honeypot by construction: /// - The tax rate, mode, recipient, and split are set once in the /// constructor and are `immutable` — there is no owner and no setter, so /// they can NEVER be changed after deploy. /// - The tax is hard-capped at MAX_FEE_BPS (1%). A 99% "can't sell" /// honeypot is impossible. /// - There is no blacklist and no pause. Nothing can trap a holder. /// Tax modes: /// None — behaves like a plain fixed-supply token. /// Burn — the whole tax on each transfer is burned (deflationary; /// the on-chain equivalent of buy-back-and-burn without /// needing a DEX router). /// Recipient — the whole tax is sent to a fixed address (creator/treasury). /// Split — the tax is divided between burn and the recipient by /// `burnShareBps` (e.g. 5000 = 50% burned, 50% to recipient). contract LaunchToken is ERC20, ERC20Burnable { enum FeeMode { None, Burn, Recipient, Split } uint8 private immutable _decimals; uint16 public constant MAX_FEE_BPS = 100; // 1.00% hard cap on the tax uint16 public constant BPS_DENOM = 10_000; // basis-point denominator uint16 public immutable feeBps; // tax, in basis points of a transfer FeeMode public immutable feeMode; address public immutable feeRecipient; // used in Recipient and Split modes uint16 public immutable burnShareBps; // share of the TAX that is burned // (Split mode); 0..BPS_DENOM // Launch fee (injected at compile from contracts/fee.config.json). // Defaults DISABLE the fee; the launchpad injects real values for mainnet. // When enabled, the fee is paid atomically in the payable constructor and // forwarded to the treasury — the launchpad never custodies it. address public constant LAUNCH_TREASURY = address(0); uint256 public constant LAUNCH_FEE_WEI = 0; // holder / flat launch fee uint256 public constant LAUNCH_FEE_BASE = 0; // non-holder fee (used only when FLEDGE + threshold set) address public constant FLEDGE = address(0); // FLEDGE token; address(0) = flat fee for everyone uint256 public constant HOLD_THRESHOLD = 0; // FLEDGE (base units) required for the holder fee /// @dev Transfers where either side is exempt are never taxed. Set once in /// the constructor (initial holder + fee recipient) with no setter, so /// the exemption set is effectively immutable. mapping(address => bool) public feeExempt; /// @param name_ Token name (e.g. "SolFlip") /// @param symbol_ Token symbol (e.g. "SFLIP") /// @param decimals_ Token decimals (18 is the EVM default) /// @param totalSupply_ Whole-token supply; minted in full to `recipient` /// @param recipient Address that receives the entire initial supply /// @param feeBps_ Transfer tax in basis points (0..MAX_FEE_BPS) /// @param feeMode_ 0 None, 1 Burn, 2 Recipient, 3 Split /// @param feeRecipient_ Tax destination for Recipient/Split modes /// @param burnShareBps_ Split mode: portion of the tax that is burned /// (0..BPS_DENOM), remainder goes to feeRecipient constructor( string memory name_, string memory symbol_, uint8 decimals_, uint256 totalSupply_, address recipient, uint16 feeBps_, FeeMode feeMode_, address feeRecipient_, uint16 burnShareBps_ ) payable ERC20(name_, symbol_) { // Launch fee (enforced only when a treasury is configured). Holders of // >= HOLD_THRESHOLD FLEDGE pay the discounted LAUNCH_FEE_WEI; everyone // else pays LAUNCH_FEE_BASE. When FLEDGE isn't configured it's a flat // LAUNCH_FEE_WEI for all. The whole msg.value is forwarded to the treasury. if (LAUNCH_TREASURY != address(0)) { uint256 fee = LAUNCH_FEE_WEI; if (FLEDGE != address(0) && HOLD_THRESHOLD > 0 && IERC20(FLEDGE).balanceOf(msg.sender) < HOLD_THRESHOLD) { fee = LAUNCH_FEE_BASE; } if (fee > 0) { require(msg.value >= fee, "launch fee required"); (bool ok, ) = LAUNCH_TREASURY.call{value: msg.value}(""); require(ok, "fee transfer failed"); } } require(recipient != address(0), "recipient is zero address"); require(totalSupply_ > 0, "supply must be > 0"); require(feeBps_ <= MAX_FEE_BPS, "fee exceeds 1% cap"); if (feeMode_ == FeeMode.None || feeBps_ == 0) { feeBps = 0; feeMode = FeeMode.None; feeRecipient = address(0); burnShareBps = 0; } else if (feeMode_ == FeeMode.Burn) { feeBps = feeBps_; feeMode = FeeMode.Burn; feeRecipient = address(0); burnShareBps = BPS_DENOM; // 100% of the tax burned (informational) } else if (feeMode_ == FeeMode.Recipient) { require(feeRecipient_ != address(0), "fee recipient is zero"); feeBps = feeBps_; feeMode = FeeMode.Recipient; feeRecipient = feeRecipient_; burnShareBps = 0; // 0% burned (informational) } else { require(feeRecipient_ != address(0), "fee recipient is zero"); require(burnShareBps_ <= BPS_DENOM, "burn share > 100%"); feeBps = feeBps_; feeMode = FeeMode.Split; feeRecipient = feeRecipient_; burnShareBps = burnShareBps_; } _decimals = decimals_; // Immutable exemptions: the initial holder (so liquidity seeding isn't // taxed) and the fee recipient (so collected fees aren't re-taxed). feeExempt[recipient] = true; if (feeRecipient != address(0)) feeExempt[feeRecipient] = true; _mint(recipient, totalSupply_ * (10 ** uint256(decimals_))); } function decimals() public view override returns (uint8) { return _decimals; } /// @dev Central transfer hook (OZ v5). Mints, burns, and exempt parties pay /// no tax; everyone else pays `feeBps`, routed by `feeMode`. function _update(address from, address to, uint256 value) internal override { if ( feeMode == FeeMode.None || from == address(0) || // mint to == address(0) || // burn feeExempt[from] || feeExempt[to] ) { super._update(from, to, value); return; } uint256 fee = (value * feeBps) / BPS_DENOM; if (fee > 0) { if (feeMode == FeeMode.Burn) { super._update(from, address(0), fee); // burn: reduces totalSupply } else if (feeMode == FeeMode.Recipient) { super._update(from, feeRecipient, fee); } else { // Split: burnPart + recipPart == fee exactly (no rounding loss). uint256 burnPart = (fee * burnShareBps) / BPS_DENOM; uint256 recipPart = fee - burnPart; if (burnPart > 0) super._update(from, address(0), burnPart); if (recipPart > 0) super._update(from, feeRecipient, recipPart); } } super._update(from, to, value - fee); } }