Skip to main content
PulseScanner.io

Address

0x8aa799489e976d003175cbf835fc49e91ca8d5ea
Current Holdings
$0.00
Total TXs
7
sent 0 + received 7
First Active
2026-09-15
block 27,554,259
Last Active
1 day ago
block 27,556,316
Funded By
not identified

Net worth historyi

No net-worth snapshots recorded yet
exact matchTokenFactorysolc 0.8.34+commit.80d5c536runtime exact · creation exact
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;


abstract contract Context {
    function _msgSender() internal view virtual returns (address) {
        return msg.sender;
    }
}

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

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

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

interface IERC20Errors {
    error ERC20InsufficientBalance(address sender, uint256 balance, uint256 needed);
    error ERC20InvalidSender(address sender);
    error ERC20InvalidReceiver(address receiver);
    error ERC20InsufficientAllowance(address spender, uint256 allowance, uint256 needed);
    error ERC20InvalidApprover(address approver);
    error ERC20InvalidSpender(address spender);
}

/// @dev Minimal UniswapV2Router02-compatible interface. PulseX e a maioria
/// dos forks da PulseChain (incl. NineInch) implementam essa mesma forma,
/// mantendo o nome WPLS(). Confira contra o router real do DEX alvo antes
/// de implantar.
interface IDexRouter02 {
    function factory() external pure returns (address);
    function WPLS() external pure returns (address);

    function swapExactETHForTokens(
        uint256 amountOutMin,
        address[] calldata path,
        address to,
        uint256 deadline
    ) external payable returns (uint256[] memory amounts);

    function addLiquidity(
        address tokenA,
        address tokenB,
        uint256 amountADesired,
        uint256 amountBDesired,
        uint256 amountAMin,
        uint256 amountBMin,
        address to,
        uint256 deadline
    ) external returns (uint256 amountA, uint256 amountB, uint256 liquidity);

    function addLiquidityETH(
        address token,
        uint256 amountTokenDesired,
        uint256 amountTokenMin,
        uint256 amountETHMin,
        address to,
        uint256 deadline
    ) external payable returns (uint256 amountToken, uint256 amountETH, uint256 liquidity);

    function swapExactETHForTokensSupportingFeeOnTransferTokens(
        uint256 amountOutMin,
        address[] calldata path,
        address to,
        uint256 deadline
    ) external payable;

    function swapExactTokensForTokensSupportingFeeOnTransferTokens(
        uint256 amountIn,
        uint256 amountOutMin,
        address[] calldata path,
        address to,
        uint256 deadline
    ) external;

    function swapExactTokensForETHSupportingFeeOnTransferTokens(
        uint256 amountIn,
        uint256 amountOutMin,
        address[] calldata path,
        address to,
        uint256 deadline
    ) external;
}

interface IDexFactory {
    function getPair(address tokenA, address tokenB) external view returns (address pair);
}

/// @dev Vista mínima da TokenFactory usada pelo Token pra ler o owner ("father")
/// e o dexRouter corrente no momento da migração (em vez de guardar uma cópia
/// imutável — assim, se a factory trocar de router via setDexRouter() antes de
/// um token específico migrar, a migração usa o router atualizado).
interface ITokenFactory {
    function owner() external view returns (address);
    function dexRouter() external view returns (IDexRouter02);
}

abstract contract ERC20 is Context, IERC20, IERC20Metadata, IERC20Errors {
    mapping(address => uint256) private _balances;
    mapping(address => mapping(address => uint256)) private _allowances;
    uint256 private _totalSupply;
    string private _name;
    string private _symbol;

    constructor(string memory name_, string memory symbol_) {
        _name = name_;
        _symbol = symbol_;
    }

    function name() public view virtual returns (string memory) { return _name; }
    function symbol() public view virtual returns (string memory) { return _symbol; }
    function decimals() public view virtual returns (uint8) { return 18; }
    function totalSupply() public view virtual returns (uint256) { return _totalSupply; }
    function balanceOf(address account) public view virtual returns (uint256) { return _balances[account]; }

    function transfer(address to, uint256 value) public virtual returns (bool) {
        _transfer(_msgSender(), to, value);
        return true;
    }

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

    function approve(address spender, uint256 value) public virtual returns (bool) {
        _approve(_msgSender(), spender, value);
        return true;
    }

    function transferFrom(address from, address to, uint256 value) public virtual returns (bool) {
        _spendAllowance(from, _msgSender(), value);
        _transfer(from, to, value);
        return true;
    }

    function _transfer(address from, address to, uint256 value) internal virtual {
        if (from == address(0)) revert ERC20InvalidSender(address(0));
        if (to == address(0)) revert ERC20InvalidReceiver(address(0));
        _update(from, to, value);
    }

    function _update(address from, address to, uint256 value) internal virtual {
        if (from == address(0)) {
            _totalSupply += value;
        } else {
            uint256 fromBalance = _balances[from];
            if (fromBalance < value) revert ERC20InsufficientBalance(from, fromBalance, value);
            unchecked { _balances[from] = fromBalance - value; }
        }
        if (to == address(0)) {
            unchecked { _totalSupply -= value; }
        } else {
            unchecked { _balances[to] += value; }
        }
        emit Transfer(from, to, value);
    }

    function _mint(address account, uint256 value) internal {
        if (account == address(0)) revert ERC20InvalidReceiver(address(0));
        _update(address(0), account, value);
    }

    function _approve(address owner, address spender, uint256 value) internal {
        _approve(owner, spender, value, true);
    }

    function _approve(address owner, address spender, uint256 value, bool emitEvent) internal virtual {
        if (owner == address(0)) revert ERC20InvalidApprover(address(0));
        if (spender == address(0)) revert ERC20InvalidSpender(address(0));
        _allowances[owner][spender] = value;
        if (emitEvent) emit Approval(owner, spender, value);
    }

    function _spendAllowance(address owner, address spender, uint256 value) internal virtual {
        uint256 currentAllowance = allowance(owner, spender);
        if (currentAllowance != type(uint256).max) {
            if (currentAllowance < value) revert ERC20InsufficientAllowance(spender, currentAllowance, value);
            unchecked { _approve(owner, spender, currentAllowance - value, false); }
        }
    }
}

abstract contract Ownable is Context {
    address private _owner;
    error OwnableUnauthorizedAccount(address account);
    error OwnableInvalidOwner(address owner);
    event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);

    constructor(address initialOwner) {
        if (initialOwner == address(0)) revert OwnableInvalidOwner(address(0));
        _transferOwnership(initialOwner);
    }

    modifier onlyOwner() { _checkOwner(); _; }

    function owner() public view virtual returns (address) { return _owner; }

    function _checkOwner() internal view virtual {
        if (owner() != _msgSender()) revert OwnableUnauthorizedAccount(_msgSender());
    }

    function transferOwnership(address newOwner) public virtual onlyOwner {
        if (newOwner == address(0)) revert OwnableInvalidOwner(address(0));
        _transferOwnership(newOwner);
    }

    function _transferOwnership(address newOwner) internal virtual {
        address oldOwner = _owner;
        _owner = newOwner;
        emit OwnershipTransferred(oldOwner, newOwner);
    }
}

abstract contract ReentrancyGuard {
    uint256 private constant _NOT_ENTERED = 1;
    uint256 private constant _ENTERED = 2;
    uint256 private _status = _NOT_ENTERED;

    error ReentrancyGuardReentrantCall();

    modifier nonReentrant() {
        if (_status == _ENTERED) revert ReentrancyGuardReentrantCall();
        _status = _ENTERED;
        _;
        _status = _NOT_ENTERED;
    }
}

/* ------------------------------------------------------------------------ */
/*                               QuoteRegistry                              */
/* ------------------------------------------------------------------------ */

error QuoteRegistry_AlreadyExists();
error QuoteRegistry_NotFound();
error QuoteRegistry_ZeroAddress();
error QuoteRegistry_InvalidNativeFlag();

/// @notice Allowlist de ativos de cotação que um token pode usar como par —
/// tanto PLS nativo (isNative = true, token = address(0)) quanto qualquer
/// ERC20 (DAI, USDT, etc). Cada quote carrega seus próprios parâmetros de
/// curva (reservas virtuais iniciais) e seu próprio threshold de migração,
/// então quotes com escalas de valor bem diferentes podem conviver sem
/// precisar de lógica especial no Token.
contract QuoteRegistry is Ownable {
    struct QuoteInfo {
        address token;                     // endereço do quote-token ERC20, ou address(0) se isNative
        bool isNative;                     // true = par nativo em PLS (sem quote-token ERC20)
        uint8 decimals;
        uint256 migrationThreshold;        // quanto de quote REAL (unidades brutas) acumulado na curva dispara a migração
        uint256 virtualQuoteReserveInit;   // reserva virtual inicial de quote -- define preço inicial / curvatura
        uint256 virtualTokenReserveInit;   // reserva virtual inicial de token -- define preço inicial / curvatura
        string label;
        bool active;
    }

    mapping(bytes32 => QuoteInfo) private _quotes;
    mapping(bytes32 => bool) private _exists;
    bytes32[] public quoteIds;

    event QuoteAdded(bytes32 indexed id, address indexed token, bool isNative, string label);
    event QuoteUpdated(bytes32 indexed id);
    event QuoteActiveSet(bytes32 indexed id, bool active);

    constructor(address initialOwner) Ownable(initialOwner) {}

    function addQuote(
        bytes32 id,
        address token,
        bool isNative,
        uint8 tokenDecimals,
        uint256 migrationThreshold,
        uint256 virtualQuoteReserveInit,
        uint256 virtualTokenReserveInit,
        string calldata label
    ) external onlyOwner {
        if (isNative && token != address(0)) revert QuoteRegistry_InvalidNativeFlag();
        if (!isNative && token == address(0)) revert QuoteRegistry_ZeroAddress();
        if (_exists[id]) revert QuoteRegistry_AlreadyExists();

        _quotes[id] = QuoteInfo({
            token: token,
            isNative: isNative,
            decimals: tokenDecimals,
            migrationThreshold: migrationThreshold,
            virtualQuoteReserveInit: virtualQuoteReserveInit,
            virtualTokenReserveInit: virtualTokenReserveInit,
            label: label,
            active: true
        });
        _exists[id] = true;
        quoteIds.push(id);

        emit QuoteAdded(id, token, isNative, label);
    }

    function setActive(bytes32 id, bool active) external onlyOwner {
        if (!_exists[id]) revert QuoteRegistry_NotFound();
        _quotes[id].active = active;
        emit QuoteActiveSet(id, active);
    }

    function updateCurveParams(
        bytes32 id,
        uint256 migrationThreshold,
        uint256 virtualQuoteReserveInit,
        uint256 virtualTokenReserveInit
    ) external onlyOwner {
        if (!_exists[id]) revert QuoteRegistry_NotFound();
        QuoteInfo storage q = _quotes[id];
        q.migrationThreshold = migrationThreshold;
        q.virtualQuoteReserveInit = virtualQuoteReserveInit;
        q.virtualTokenReserveInit = virtualTokenReserveInit;
        emit QuoteUpdated(id);
    }

    function updateLabel(bytes32 id, string calldata label) external onlyOwner {
        if (!_exists[id]) revert QuoteRegistry_NotFound();
        _quotes[id].label = label;
        emit QuoteUpdated(id);
    }

    function getQuote(bytes32 id) external view returns (QuoteInfo memory) {
        if (!_exists[id]) revert QuoteRegistry_NotFound();
        return _quotes[id];
    }

    function allQuoteIds() external view returns (bytes32[] memory) {
        return quoteIds;
    }
}

/* ------------------------------------------------------------------------ */
/*                                   Token                                  */
/* ------------------------------------------------------------------------ */

error Token_NotFactory();
error Token_ZeroAddress();
error Token_CurveNotActive();
error Token_CurveClosed();
error Token_DeadlineExpired();
error Token_ZeroAmount();
error Token_SlippageExceeded();
error Token_UnexpectedValue();
error Token_CurveSupplyExceeded();
error Token_AlreadyMigrated();
error Token_ThresholdNotReached();
error Token_TransferFailed();
error Token_TransfersLockedDuringCurve();
error Token_InsufficientCurveLiquidity();
error Token_OnlySelf();

contract Token is ERC20, ReentrancyGuard {
    uint256 public constant INITIAL_SUPPLY = 1_000_000_000 * 10 ** 18;
    address public constant DEAD = 0x000000000000000000000000000000000000dEaD;
    address public constant NATIVE = address(0);

    // --- alocação da curva ---
    // 80% do supply é vendável na curva; os 20% restantes ficam travados no
    // contrato até a migração. Na migração, TODO o saldo restante do
    // contrato (>= MIGRATION_TOKEN_RESERVE_MIN, pode ser mais se o threshold
    // bater antes de esgotar a alocação da curva) vira liquidez -- assim
    // nenhum token fica preso pra sempre no contrato.
    uint256 public constant CURVE_TOKEN_ALLOCATION = 800_000_000 * 10 ** 18;
    uint256 public constant MIGRATION_TOKEN_RESERVE_MIN = 200_000_000 * 10 ** 18;

    // ---- taxas, em pontos-base de 10_000 ----
    uint16 public constant BUY_FATHER_FEE_BP = 70;    // 0.7% -> owner da factory ("father")
    uint16 public constant BUY_CREATOR_FEE_BP = 30;   // 0.3% -> criador do token
    uint16 public constant SELL_CREATOR_FEE_BP = 30;  // 0.3% -> criador do token (sem taxa de father na venda)

    address public immutable factory;
    address public immutable creator;
    address public immutable quoteToken; // address(0) = par nativo em PLS

    // --- estado da curva ---
    bool public curveActive;               // true = fase 1 (curva), false = fase 2 (DEX)
    bool public migrated;                  // trava _migrate() pra rodar uma única vez

    uint256 public virtualQuoteReserve;    // reserva virtual de quote (não é saldo real, é parâmetro da curva)
    uint256 public virtualTokenReserve;    // reserva virtual de tokens (idem)
    uint256 public realQuoteReserve;       // saldo REAL de quote acumulado das compras na curva
    uint256 public curveTokensSold;        // total de tokens já entregues (bruto, antes de taxa) na curva

    uint256 public immutable migrationThreshold; // quanto de quote real precisa acumular pra migrar

    address public pair;

    event Launched(address indexed pair, uint256 tokenLiquidity, uint256 quoteLiquidity);
    event FeeTaken(bool indexed isBuy, address indexed to, uint256 amount);
    event CurveBuy(address indexed buyer, address indexed to, uint256 quoteIn, uint256 tokensOut);
    event CurveSell(address indexed seller, address indexed to, uint256 tokensIn, uint256 quoteOut);

    modifier onlyFactory() {
        if (_msgSender() != factory) revert Token_NotFactory();
        _;
    }

    constructor(
        string memory _name,
        string memory _symbol,
        address _creator,
        address _factory,
        address _quoteToken,          // address(0) = par nativo em PLS
        uint256 _virtualQuoteReserveInit,
        uint256 _virtualTokenReserveInit,
        uint256 _migrationThreshold
    ) ERC20(_name, _symbol) {
        if (_factory == address(0) || _creator == address(0)) revert Token_ZeroAddress();
        if (_virtualQuoteReserveInit == 0 || _virtualTokenReserveInit == 0) revert Token_ZeroAmount();

        factory = _factory;
        creator = _creator;
        quoteToken = _quoteToken;

        virtualQuoteReserve = _virtualQuoteReserveInit;
        virtualTokenReserve = _virtualTokenReserveInit;
        migrationThreshold = _migrationThreshold;

        curveActive = true;

        // todo o supply nasce dentro do próprio contrato: 80% vendável na
        // curva, 20% travado até a migração. Nada de "launch()" separado --
        // a curva já nasce ativa.
        _mint(address(this), INITIAL_SUPPLY);
    }

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

    /* ------------------------------ curva --------------------------------- */

    /// @param amountIn Quantidade de quote-token ERC20 a gastar. Ignorado
    ///        (use msg.value) se quoteToken == NATIVE.
    /// @param minTokensOut Proteção de slippage.
    /// @param to Endereço que recebe os tokens comprados (permite roteamento
    ///        via SwapRouter em nome do usuário final).
    function buyOnCurve(
        uint256 amountIn,
        uint256 minTokensOut,
        address to,
        uint256 deadline
    ) external payable nonReentrant returns (uint256 tokensOut) {
        if (!curveActive) revert Token_CurveNotActive();
        if (block.timestamp > deadline) revert Token_DeadlineExpired();
        if (to == address(0)) revert Token_ZeroAddress();

        address payer = _msgSender();
        uint256 quoteIn;

        if (quoteToken == NATIVE) {
            quoteIn = msg.value;
            if (quoteIn == 0) revert Token_ZeroAmount();
        } else {
            if (msg.value != 0) revert Token_UnexpectedValue();
            quoteIn = amountIn;
            if (quoteIn == 0) revert Token_ZeroAmount();
            IERC20(quoteToken).transferFrom(payer, address(this), quoteIn);
        }

        // produto constante virtual: dx * dy = k
        uint256 grossTokensOut = virtualTokenReserve -
            (virtualQuoteReserve * virtualTokenReserve) / (virtualQuoteReserve + quoteIn);

        if (curveTokensSold + grossTokensOut > CURVE_TOKEN_ALLOCATION) revert Token_CurveSupplyExceeded();

        uint256 fatherFee = (grossTokensOut * BUY_FATHER_FEE_BP) / 10_000;
        uint256 creatorFee = (grossTokensOut * BUY_CREATOR_FEE_BP) / 10_000;
        tokensOut = grossTokensOut - fatherFee - creatorFee;

        if (tokensOut < minTokensOut) revert Token_SlippageExceeded();

        virtualQuoteReserve += quoteIn;
        virtualTokenReserve -= grossTokensOut;
        realQuoteReserve += quoteIn;
        curveTokensSold += grossTokensOut;

        address fatherAddr = ITokenFactory(factory).owner();
        if (fatherFee > 0) {
            _update(address(this), fatherAddr, fatherFee);
            emit FeeTaken(true, fatherAddr, fatherFee);
        }
        if (creatorFee > 0) {
            _update(address(this), creator, creatorFee);
            emit FeeTaken(true, creator, creatorFee);
        }
        _update(address(this), to, tokensOut);

        emit CurveBuy(payer, to, quoteIn, tokensOut);

        if (realQuoteReserve >= migrationThreshold) {
            // Tenta migrar automaticamente dentro da própria compra. Isso é
            // feito via chamada externa (this._selfMigrate) dentro de um
            // try/catch de propósito: se o addLiquidity[ETH] no DEX reverter
            // por qualquer motivo (router mal configurado, pool com
            // problema, etc.), a COMPRA continua valendo -- o comprador
            // recebe os tokens normalmente e a migração fica pendente,
            // podendo ser destravada depois por qualquer um via migrate().
            // Antes dessa mudança, uma falha na migração revertia a compra
            // inteira que cruzou o threshold, bloqueando exatamente a
            // compra que deveria fechar a curva.
            try this._selfMigrate() {} catch {}
        }
    }

    /// @param tokenAmountIn Quantidade de tokens (do próprio caller) a vender.
    /// @param to Endereço que recebe o quote de saída (nativo ou ERC20).
    function sellOnCurve(
        uint256 tokenAmountIn,
        uint256 minQuoteOut,
        address to,
        uint256 deadline
    ) external nonReentrant returns (uint256 quoteOut) {
        if (!curveActive) revert Token_CurveNotActive();
        if (block.timestamp > deadline) revert Token_DeadlineExpired();
        if (tokenAmountIn == 0) revert Token_ZeroAmount();
        if (to == address(0)) revert Token_ZeroAddress();

        address seller = _msgSender();

        uint256 creatorFee = (tokenAmountIn * SELL_CREATOR_FEE_BP) / 10_000;
        uint256 tokenAmountInAfterFee = tokenAmountIn - creatorFee;

        quoteOut = virtualQuoteReserve -
            (virtualQuoteReserve * virtualTokenReserve) / (virtualTokenReserve + tokenAmountInAfterFee);

        if (quoteOut < minQuoteOut) revert Token_SlippageExceeded();
        if (quoteOut > realQuoteReserve) revert Token_InsufficientCurveLiquidity();

        virtualTokenReserve += tokenAmountInAfterFee;
        virtualQuoteReserve -= quoteOut;
        realQuoteReserve -= quoteOut;
        curveTokensSold -= tokenAmountInAfterFee > curveTokensSold ? curveTokensSold : tokenAmountInAfterFee;

        // puxa os tokens do vendedor via _update interno -- bypassa o lock de
        // _transfer (que só se aplica a transfer()/transferFrom() públicos).
        _update(seller, address(this), tokenAmountInAfterFee);
        if (creatorFee > 0) {
            _update(seller, creator, creatorFee);
            emit FeeTaken(false, creator, creatorFee);
        }

        _payoutQuote(to, quoteOut);

        emit CurveSell(seller, to, tokenAmountIn, quoteOut);
    }

    /// @notice Fallback permissionless: qualquer um pode disparar a migração
    /// se o threshold já foi atingido mas a auto-chamada dentro de
    /// buyOnCurve não rodou (ex: falha de gas naquela tx específica).
    function migrate() external nonReentrant {
        if (!curveActive) revert Token_CurveNotActive();
        if (realQuoteReserve < migrationThreshold) revert Token_ThresholdNotReached();
        _migrate();
    }

    /// @dev Só pode ser chamado pelo próprio contrato, via `try this._selfMigrate()`
    /// dentro de buyOnCurve. Não leva o modifier nonReentrant de propósito:
    /// já estamos dentro de uma chamada protegida por nonReentrant
    /// (buyOnCurve), então reentrância já está bloqueada nesse ponto --
    /// colocar o modifier aqui faria essa chamada sempre reverter com
    /// ReentrancyGuardReentrantCall, e o catch engoliria isso silenciosamente
    /// (a migração nunca aconteceria pela compra, só pelo migrate() manual).
    function _selfMigrate() external {
        if (msg.sender != address(this)) revert Token_OnlySelf();
        _migrate();
    }

    function _migrate() internal {
        if (!curveActive || migrated) revert Token_AlreadyMigrated();

        curveActive = false;
        migrated = true;

        uint256 quoteForLiquidity = realQuoteReserve;
        realQuoteReserve = 0;

        // todo o saldo de tokens que sobrou no contrato vira liquidez --
        // nunca menos que MIGRATION_TOKEN_RESERVE_MIN, pode ser mais se o
        // threshold bateu antes de esgotar CURVE_TOKEN_ALLOCATION.
        uint256 tokensForLiquidity = balanceOf(address(this));

        IDexRouter02 dexRouter = ITokenFactory(factory).dexRouter();
        address newPair;

        _approve(address(this), address(dexRouter), tokensForLiquidity);

        if (quoteToken == NATIVE) {
            dexRouter.addLiquidityETH{value: quoteForLiquidity}(
                address(this),
                tokensForLiquidity,
                0,
                0,
                DEAD, // LP queimada permanentemente
                block.timestamp
            );
            newPair = IDexFactory(dexRouter.factory()).getPair(address(this), dexRouter.WPLS());
        } else {
            IERC20(quoteToken).approve(address(dexRouter), quoteForLiquidity);
            dexRouter.addLiquidity(
                address(this),
                quoteToken,
                tokensForLiquidity,
                quoteForLiquidity,
                0,
                0,
                DEAD, // LP queimada permanentemente
                block.timestamp
            );
            newPair = IDexFactory(dexRouter.factory()).getPair(address(this), quoteToken);
        }

        pair = newPair;
        emit Launched(newPair, tokensForLiquidity, quoteForLiquidity);
    }

    function _payoutQuote(address to, uint256 amount) internal {
        if (quoteToken == NATIVE) {
            (bool success, ) = payable(to).call{value: amount}("");
            if (!success) revert Token_TransferFailed();
        } else {
            IERC20(quoteToken).transfer(to, amount);
        }
    }

    /* ---------------------------- fee logic ------------------------------ */

    function _transfer(address from, address to, uint256 value) internal override {
        // durante a fase de curva, transferências públicas (transfer /
        // transferFrom) ficam travadas -- só buyOnCurve/sellOnCurve movem
        // saldo, via _update interno, que não passa por aqui. Isso evita um
        // mercado secundário paralelo fora da curva antes da migração.
        if (curveActive) revert Token_TransfersLockedDuringCurve();
        super._transfer(from, to, value);
    }

    function _update(address from, address to, uint256 value) internal override {
        if (migrated && pair != address(0) && value > 0) {
            if (from == pair) {
                // COMPRA: pair -> destinatário
                uint256 fatherFee = (value * BUY_FATHER_FEE_BP) / 10_000;
                uint256 creatorFee = (value * BUY_CREATOR_FEE_BP) / 10_000;
                uint256 amountAfterFee = value - fatherFee - creatorFee;

                address fatherAddr = ITokenFactory(factory).owner();

                if (fatherFee > 0) {
                    super._update(from, fatherAddr, fatherFee);
                    emit FeeTaken(true, fatherAddr, fatherFee);
                }
                if (creatorFee > 0) {
                    super._update(from, creator, creatorFee);
                    emit FeeTaken(true, creator, creatorFee);
                }
                super._update(from, to, amountAfterFee);
                return;
            } else if (to == pair) {
                // VENDA: remetente -> pair
                uint256 creatorFee = (value * SELL_CREATOR_FEE_BP) / 10_000;
                uint256 amountAfterFee = value - creatorFee;

                if (creatorFee > 0) {
                    super._update(from, creator, creatorFee);
                    emit FeeTaken(false, creator, creatorFee);
                }
                super._update(from, to, amountAfterFee);
                return;
            }
        }

        super._update(from, to, value);
    }

    receive() external payable {}
}

/* ------------------------------------------------------------------------ */
/*                                 Factory                                  */
/* ------------------------------------------------------------------------ */

error Factory_QuoteNotActive();
error Factory_NotFromFactory();
error Factory_NotCreator();
error Factory_NoDefaultQuote();

contract TokenFactory is Ownable {
    struct TokenInfo {
        address creator;
        bytes32 quoteId;
        address quoteToken;
        string description;
        string twitter;
        string telegram;
        string website;
        string image;
    }

    // Sem swap de seed, sem compra especial pro criador, sem launch()
    // imediato -- create() só faz o deploy. A curva já nasce ativa dentro
    // do próprio Token. Se o criador quiser tokens, ele chama
    // Token.buyOnCurve() como qualquer outro comprador, sem tratamento
    // especial nem fatia reservada.
    struct CreateParams {
        string name;
        string symbol;
        bytes32 quoteId;
        string description;
        string twitter;
        string telegram;
        string website;
        string image;
    }

    event TokenCreated(
        address indexed token,
        address indexed creator,
        bytes32 indexed quoteId,
        address quoteToken,
        string name,
        string symbol
    );
    event TokenInfoUpdated(address indexed token);
    event DexRouterUpdated(address indexed oldRouter, address indexed newRouter);
    event DefaultQuoteIdUpdated(bytes32 indexed oldId, bytes32 indexed newId);

    QuoteRegistry public immutable quoteRegistry;
    IDexRouter02 public dexRouter;

    // Quote usada quando o criador não escolhe nenhuma explicitamente
    // (p.quoteId == bytes32(0) em create()). Pareamento com um token
    // específico (DAI, USDT etc.) vira opcional -- por padrão o token nasce
    // pareado com essa quote (tipicamente a nativa, PLS). O owner define
    // qual é via setDefaultQuoteId().
    bytes32 public defaultQuoteId;

    address[] public allTokens;
    mapping(address => bool) public isTokenFromFactory;
    mapping(address => TokenInfo) public tokenInfo;

    constructor(address _quoteRegistry, address _dexRouter, bytes32 _defaultQuoteId) Ownable(msg.sender) {
        quoteRegistry = QuoteRegistry(_quoteRegistry);
        dexRouter = IDexRouter02(_dexRouter);
        defaultQuoteId = _defaultQuoteId;
    }

    function setDexRouter(address newRouter) external onlyOwner {
        emit DexRouterUpdated(address(dexRouter), newRouter);
        dexRouter = IDexRouter02(newRouter);
    }

    /// @notice Define qual quote é usada quando o criador não escolhe
    /// nenhuma em create() (quoteId == bytes32(0)). Precisa já existir e
    /// estar ativa no QuoteRegistry.
    function setDefaultQuoteId(bytes32 newId) external onlyOwner {
        QuoteRegistry.QuoteInfo memory q = quoteRegistry.getQuote(newId); // reverte se não existir
        if (!q.active) revert Factory_QuoteNotActive();
        emit DefaultQuoteIdUpdated(defaultQuoteId, newId);
        defaultQuoteId = newId;
    }

    /// @notice Deploy simples do token. A curva já nasce ativa, configurada
    /// com os parâmetros do quote escolhido. Nenhum PLS/valor é recebido
    /// aqui -- compras acontecem depois, direto em Token.buyOnCurve() (ou via
    /// SwapRouter.buy()).
    function create(CreateParams calldata p) external returns (address tokenAddr) {
        // Pareamento com um quote específico é opcional: se o criador não
        // passar quoteId (deixar bytes32(0)), usa a quote padrão da
        // factory (defaultQuoteId, definida por setDefaultQuoteId()).
        bytes32 quoteIdToUse = p.quoteId == bytes32(0) ? defaultQuoteId : p.quoteId;
        if (quoteIdToUse == bytes32(0)) revert Factory_NoDefaultQuote();

        QuoteRegistry.QuoteInfo memory q = quoteRegistry.getQuote(quoteIdToUse);
        if (!q.active) revert Factory_QuoteNotActive();

        Token token = new Token(
            p.name,
            p.symbol,
            _msgSender(),
            address(this),
            q.token,
            q.virtualQuoteReserveInit,
            q.virtualTokenReserveInit,
            q.migrationThreshold
        );
        tokenAddr = address(token);

        _recordToken(tokenAddr, p, quoteIdToUse, q.token);
    }

    function mintFor(address token, address to, uint256 amount) external onlyOwner {
        require(isTokenFromFactory[token], "token nao veio deste factory");
        Token(payable(token)).mint(to, amount);
    }

    function _recordToken(
        address tokenAddr,
        CreateParams calldata p,
        bytes32 quoteIdUsed,
        address quoteTokenAddr
    ) internal {
        isTokenFromFactory[tokenAddr] = true;
        allTokens.push(tokenAddr);
        tokenInfo[tokenAddr] = TokenInfo({
            creator: _msgSender(),
            quoteId: quoteIdUsed,
            quoteToken: quoteTokenAddr,
            description: p.description,
            twitter: p.twitter,
            telegram: p.telegram,
            website: p.website,
            image: p.image
        });

        emit TokenCreated(tokenAddr, _msgSender(), quoteIdUsed, quoteTokenAddr, p.name, p.symbol);
    }

    function updateTokenInfo(
        address token,
        string calldata description,
        string calldata twitter,
        string calldata telegram,
        string calldata website,
        string calldata image
    ) external {
        if (!isTokenFromFactory[token]) revert Factory_NotFromFactory();
        if (tokenInfo[token].creator != _msgSender()) revert Factory_NotCreator();

        TokenInfo storage info = tokenInfo[token];
        info.description = description;
        info.twitter = twitter;
        info.telegram = telegram;
        info.website = website;
        info.image = image;

        emit TokenInfoUpdated(token);
    }

    function totalTokensCreated() external view returns (uint256) { return allTokens.length; }
    function getAllTokens() external view returns (address[] memory) { return allTokens; }
    function getTokenInfo(address token) external view returns (TokenInfo memory) { return tokenInfo[token]; }
}

/* ------------------------------------------------------------------------ */
/*                                SwapRouter                                */
/* ------------------------------------------------------------------------ */

error SwapRouter_UnsupportedInput();
error SwapRouter_UnsupportedOutput();
error SwapRouter_BadValue();
error SwapRouter_SellDuringCurveUnsupported();

/// @notice Entrypoint de negociação voltado pro usuário. `address(0)` é a
/// convenção pra "PLS nativo" tanto do lado de entrada (compra) quanto de
/// saída (venda) -- inclusive quando o próprio par do token É nativo.
///
/// IMPORTANTE: durante a fase de curva, sell() não funciona -- o token trava
/// transferências públicas nessa fase (ver Token._transfer), então o router
/// não consegue puxar o saldo do usuário via transferFrom. Pra vender
/// durante a curva, chame Token.sellOnCurve() diretamente. buy() funciona
/// normalmente nas duas fases, porque só movimenta o quote-token (ou PLS
/// nativo), nunca o token da curva em si.
contract SwapRouter is ReentrancyGuard {
    address public constant NATIVE = address(0);

    TokenFactory public immutable factory;

    constructor(address _factory) {
        factory = TokenFactory(_factory);
    }

    /// @param inputToken NATIVE (paga com PLS) ou o próprio quoteToken do token (paga com o ERC20 quote direto)
    function buy(
        address token,
        uint256 amountIn,
        uint256 minOut,
        address inputToken,
        uint256 deadline
    ) external payable nonReentrant {
        address quote = Token(payable(token)).quoteToken();
        bool curveActive = Token(payable(token)).curveActive();

        if (curveActive) {
            if (quote == NATIVE) {
                // curva nativa: só aceita PLS diretamente, sem conversão --
                // não existe pool ainda pra converter outro ativo em PLS.
                if (inputToken != NATIVE) revert SwapRouter_UnsupportedInput();
                if (msg.value == 0) revert SwapRouter_BadValue();
                Token(payable(token)).buyOnCurve{value: msg.value}(msg.value, minOut, _msgSender(), deadline);
            } else if (inputToken == quote) {
                // curva em quote-token ERC20, pagando com o próprio quote-token.
                if (msg.value != 0) revert SwapRouter_BadValue();
                IERC20(quote).transferFrom(_msgSender(), address(this), amountIn);
                IERC20(quote).approve(token, amountIn);
                Token(payable(token)).buyOnCurve(amountIn, minOut, _msgSender(), deadline);
            } else if (inputToken == NATIVE) {
                // curva em quote-token ERC20, pagando com PLS: o router NÃO
                // usa a liquidez da própria curva (não existe ainda) -- ele
                // faz PLS -> quote via um pool EXTERNO já existente no DEX
                // (ex.: o par WPLS/DAI real da PulseX) e só então entrega o
                // quote resultante pro buyOnCurve. Isso só funciona se esse
                // pool externo já tiver liquidez suficiente; caso contrário
                // o swap reverte no próprio DEX, não no launchpad. `minOut`
                // aqui protege apenas a etapa final (buyOnCurve); a etapa do
                // swap PLS->quote não tem proteção de slippage própria.
                if (msg.value == 0) revert SwapRouter_BadValue();
                IDexRouter02 curveDex = factory.dexRouter();

                address[] memory path = new address[](2);
                path[0] = curveDex.WPLS();
                path[1] = quote;

                uint256 quoteBalBefore = IERC20(quote).balanceOf(address(this));
                curveDex.swapExactETHForTokens{value: msg.value}(0, path, address(this), deadline);
                uint256 quoteReceived = IERC20(quote).balanceOf(address(this)) - quoteBalBefore;

                IERC20(quote).approve(token, quoteReceived);
                Token(payable(token)).buyOnCurve(quoteReceived, minOut, _msgSender(), deadline);
            } else {
                revert SwapRouter_UnsupportedInput();
            }
            return;
        }

        // pós-migração: fluxo DEX de sempre
        IDexRouter02 dex = factory.dexRouter();

        if (inputToken == quote && quote != NATIVE) {
            if (msg.value != 0) revert SwapRouter_BadValue();
            IERC20(quote).transferFrom(_msgSender(), address(this), amountIn);
            IERC20(quote).approve(address(dex), amountIn);

            address[] memory path = new address[](2);
            path[0] = quote;
            path[1] = token;

            dex.swapExactTokensForTokensSupportingFeeOnTransferTokens(
                amountIn, minOut, path, _msgSender(), deadline
            );
        } else if (inputToken == NATIVE) {
            if (msg.value != amountIn) revert SwapRouter_BadValue();

            address[] memory path;
            if (quote == NATIVE) {
                path = new address[](2);
                path[0] = dex.WPLS();
                path[1] = token;
            } else {
                path = new address[](3);
                path[0] = dex.WPLS();
                path[1] = quote;
                path[2] = token;
            }

            dex.swapExactETHForTokensSupportingFeeOnTransferTokens{value: msg.value}(
                minOut, path, _msgSender(), deadline
            );
        } else {
            revert SwapRouter_UnsupportedInput();
        }
    }

    /// @param outputToken NATIVE (recebe PLS) ou o próprio quoteToken do token (recebe o ERC20 quote direto)
    function sell(
        address token,
        uint256 amountIn,
        uint256 minOut,
        address outputToken,
        uint256 deadline
    ) external nonReentrant {
        if (Token(payable(token)).curveActive()) revert SwapRouter_SellDuringCurveUnsupported();

        address quote = Token(payable(token)).quoteToken();
        IDexRouter02 dex = factory.dexRouter();

        IERC20(token).transferFrom(_msgSender(), address(this), amountIn);
        IERC20(token).approve(address(dex), amountIn);

        if (outputToken == quote && quote != NATIVE) {
            address[] memory path = new address[](2);
            path[0] = token;
            path[1] = quote;

            dex.swapExactTokensForTokensSupportingFeeOnTransferTokens(
                amountIn, minOut, path, _msgSender(), deadline
            );
        } else if (outputToken == NATIVE) {
            address[] memory path;
            if (quote == NATIVE) {
                path = new address[](2);
                path[0] = token;
                path[1] = dex.WPLS();
            } else {
                path = new address[](3);
                path[0] = token;
                path[1] = quote;
                path[2] = dex.WPLS();
            }

            dex.swapExactTokensForETHSupportingFeeOnTransferTokens(
                amountIn, minOut, path, _msgSender(), deadline
            );
        } else {
            revert SwapRouter_UnsupportedOutput();
        }
    }

    function _msgSender() internal view returns (address) {
        return msg.sender;
    }
}