Skip to main content
PulseScanner.io

Address

0xffa8f65dee3e694ac46216cdd4f4e3c6d4dbb185
Current Holdings
$0.00
TXs sent
not counted
First Active
2026-07-18
block 27,062,080
Last Active
58 days ago
block 27,078,493
Funded By
not identified

Net worth historyi

No net-worth snapshots recorded yet
exact matchParityBurnFactorysolc 0.8.26+commit.8a97fa7aruntime exact · creation exact
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

// ════════════════════════════════════════════════════════════════════════
//  ParityBurn  —  by d_f_369, parityburn
// ════════════════════════════════════════════════════════════════════════
//  EXPLORER VERIFICATION (scan.pulsechain.com / Etherscan-compatible)
//  ------------------------------------------------------------------------
//    Source File:        ParityBurn.sol   (single file, no imports,
//                        contains ALL THREE contracts below)
//    Compiler Type:      Solidity (Single file)
//    Compiler Version:   v0.8.26+commit.8a97fa7a
//    EVM Version:        paris   (REQUIRED — PulseChain rejects cancun MCOPY / shanghai PUSH0)
//    Optimization:       Yes, 200 runs
//    License:            MIT License (Expat)
//
//    Three deployed contracts, all verified with THIS same file:
//      1. ParityBurnFactory — the address YOU deploy.
//         Constructor Arguments: NONE.
//      2. ParityBurnRouter  — deployed automatically BY the factory
//         constructor (read its address from factory.router()).
//         Constructor Arguments: NONE.
//      3. ParityBurnPair    — deployed by factory.createPair() for each pool.
//         Constructor Arguments: NONE (tokens set via initialize()).
//    When verifying, pick the matching Contract Name for each address.
//
//    Hardcoded constants (router):
//      WETH (WPLS)  = 0xA1077a294dDE1B09bB078844df40758a5D0f9a27
//      BURN_PDAI    = 0x6B175474E89094C44Da98b954EedeAC495271d0F  (pDAI)
//      BURN_PWBTC   = 0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599  (pWBTC)
//      BURN_PUSDT   = 0xdAC17F958D2ee523a2206206994597C13D831ec7  (pUSDT)
//      BURN_PUSDC   = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48  (pUSDC)
//      BURN_PPAXG   = 0x45804880De22913dAFE09f4980848ECE6EcbAf78  (pPAXG)
//      BURN_PSHIB   = 0x95aD61b0a150d79219dCF64E1E6Cc01f0B64C4cE  (pSHIB)
//      BURN_PPEPE   = 0x6982508145454Ce325dDbE47a25d4ec3d2311933  (pPEPE)
//      BURN_PBNB    = 0xB8c77482e45F1F44dE1745F52C74426C631bDD52  (pBNB)
//      BURN_PENS    = 0xC18360217D8F7Ab5e7c516566761Ea12Ce7F9D72  (pENS)
//      BURN_PCOMP   = 0xc00e94Cb662C3520282E6f5717214004A7f26888  (pCOMP)
//      BURN_PAAVE   = 0x7Fc66500c84A76Ad7e9c93437bFc5Ac33E2DDaE9  (pAAVE)
//      BURN_DRK     = 0xD0784DFbD59B435Ed692f6A6D0358dc8242b11b3  (DRK)
//      BURN_ADDRESS = 0x000000000000000000000000000000000000dEaD
// ════════════════════════════════════════════════════════════════════════
//
//  WHAT THIS IS
//  ------------
//  A standalone mini-DEX on PulseChain — our OWN pools, not PulseX's.
//
//    - ParityBurnFactory creates ParityBurnPair pools and auto-deploys the
//      ParityBurnRouter in its constructor (one deploy tx for everything).
//    - Liquidity is added to OUR pairs; OUR pairs mint the LP tokens.
//      Pool prices are set by whoever deposits — deliberately allowed to
//      differ from PulseX so arbitrage bots trade the gap.
//    - Swaps are ROUTER-ONLY: pair.swap() rejects any caller that is not
//      factory.router(). The router burns 1% of every designated burn token
//      that is a swap endpoint — so an arb CANNOT bypass the burn.
//    - Liquidity providers add and remove liquidity freely at any time — no
//      lock, no restriction — and can always withdraw 100% of their tokens.
//    - LP fee is the classic 0.30% (997/1000) and accrues to the LPs.
//    - factory.setRouter() lets the owner point pools at an upgraded router
//      without migrating liquidity.
//
//  Deviations from canonical UniswapV2Pair (deliberate, for size/simplicity):
//    - No price0/price1 cumulative oracle accumulators, no kLast/protocol fee.
//    - No flash-swap callback: swap(uint,uint,address) takes no bytes param.
//    - Reentrancy lock, MINIMUM_LIQUIDITY lock, uint112 reserve cap and the
//      0.3% adjusted-K invariant check are all preserved.
// ════════════════════════════════════════════════════════════════════════

// ─── Minimal shared interfaces ───

interface IERC20 {
    function transfer(address to, uint256 value) external returns (bool);
    function transferFrom(address from, address to, uint256 value) external returns (bool);
    function balanceOf(address owner) external view returns (uint256);
    function approve(address spender, uint256 value) external returns (bool);
}

interface IWETH {
    function deposit() external payable;
    function withdraw(uint256) external;
    function transfer(address to, uint256 value) external returns (bool);
}

// ════════════════════════════════════════════════════════════════════════
//  ParityBurnPair — one pool. Holds reserves, mints/burns LP tokens.
//  Swaps only accepted from factory.router().
// ════════════════════════════════════════════════════════════════════════

contract ParityBurnPair {

    // ── LP ERC20 ──
    string public constant name = "ParityBurn LP";
    string public constant symbol = "PBLP";
    uint8 public constant decimals = 18;
    uint256 public totalSupply;
    mapping(address => uint256) public balanceOf;
    mapping(address => mapping(address => uint256)) public allowance;

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

    // ── Pool state ──
    uint256 public constant MINIMUM_LIQUIDITY = 1000;
    address public immutable factory;
    address public token0;
    address public token1;
    uint112 private reserve0;
    uint112 private reserve1;
    uint32 private blockTimestampLast;

    event Mint(address indexed sender, uint256 amount0, uint256 amount1);
    event Burn(address indexed sender, uint256 amount0, uint256 amount1, address indexed to);
    event Swap(address indexed sender, uint256 amount0In, uint256 amount1In,
               uint256 amount0Out, uint256 amount1Out, address indexed to);
    event Sync(uint112 reserve0, uint112 reserve1);

    // ── Reentrancy lock ──
    uint256 private unlocked = 1;
    modifier lock() {
        require(unlocked == 1, "ParityBurn: LOCKED");
        unlocked = 0;
        _;
        unlocked = 1;
    }

    constructor() {
        factory = msg.sender;
    }

    /// @dev Called once by the factory right after CREATE2.
    function initialize(address _token0, address _token1) external {
        require(msg.sender == factory && token0 == address(0), "ParityBurn: FORBIDDEN");
        token0 = _token0;
        token1 = _token1;
    }

    function getReserves() public view
        returns (uint112 _reserve0, uint112 _reserve1, uint32 _blockTimestampLast)
    {
        _reserve0 = reserve0;
        _reserve1 = reserve1;
        _blockTimestampLast = blockTimestampLast;
    }

    // ── LP ERC20 internals ──

    function _mint(address to, uint256 value) internal {
        totalSupply += value;
        balanceOf[to] += value;
        emit Transfer(address(0), to, value);
    }

    function _burn(address from, uint256 value) internal {
        balanceOf[from] -= value;
        totalSupply -= value;
        emit Transfer(from, address(0), value);
    }

    function approve(address spender, uint256 value) external returns (bool) {
        allowance[msg.sender][spender] = value;
        emit Approval(msg.sender, spender, value);
        return true;
    }

    function transfer(address to, uint256 value) external returns (bool) {
        balanceOf[msg.sender] -= value;
        balanceOf[to] += value;
        emit Transfer(msg.sender, to, value);
        return true;
    }

    function transferFrom(address from, address to, uint256 value) external returns (bool) {
        if (allowance[from][msg.sender] != type(uint256).max) {
            allowance[from][msg.sender] -= value;
        }
        balanceOf[from] -= value;
        balanceOf[to] += value;
        emit Transfer(from, to, value);
        return true;
    }

    // ── Pool logic ──

    function _update(uint256 balance0, uint256 balance1) private {
        require(balance0 <= type(uint112).max && balance1 <= type(uint112).max,
                "ParityBurn: OVERFLOW");
        reserve0 = uint112(balance0);
        reserve1 = uint112(balance1);
        blockTimestampLast = uint32(block.timestamp);
        emit Sync(reserve0, reserve1);
    }

    /// @notice Mint LP tokens for whatever was transferred in since last sync.
    ///         Standard V2 — open access (adding liquidity carries no burn).
    function mint(address to) external lock returns (uint256 liquidity) {
        (uint112 _reserve0, uint112 _reserve1,) = getReserves();
        uint256 balance0 = IERC20(token0).balanceOf(address(this));
        uint256 balance1 = IERC20(token1).balanceOf(address(this));
        uint256 amount0 = balance0 - _reserve0;
        uint256 amount1 = balance1 - _reserve1;

        uint256 _totalSupply = totalSupply;
        if (_totalSupply == 0) {
            liquidity = _sqrt(amount0 * amount1) - MINIMUM_LIQUIDITY;
            _mint(address(0), MINIMUM_LIQUIDITY); // permanently locked
        } else {
            liquidity = _min(amount0 * _totalSupply / _reserve0,
                             amount1 * _totalSupply / _reserve1);
        }
        require(liquidity > 0, "ParityBurn: INSUFFICIENT_LIQUIDITY_MINTED");
        _mint(to, liquidity);

        _update(balance0, balance1);
        emit Mint(msg.sender, amount0, amount1);
    }

    /// @notice Burn LP tokens held by this contract, send underlying to `to`.
    function burn(address to) external lock returns (uint256 amount0, uint256 amount1) {
        address _token0 = token0;
        address _token1 = token1;
        uint256 balance0 = IERC20(_token0).balanceOf(address(this));
        uint256 balance1 = IERC20(_token1).balanceOf(address(this));
        uint256 liquidity = balanceOf[address(this)];

        uint256 _totalSupply = totalSupply;
        amount0 = liquidity * balance0 / _totalSupply;
        amount1 = liquidity * balance1 / _totalSupply;
        require(amount0 > 0 && amount1 > 0, "ParityBurn: INSUFFICIENT_LIQUIDITY_BURNED");
        _burn(address(this), liquidity);
        _safeTransfer(_token0, to, amount0);
        _safeTransfer(_token1, to, amount1);
        balance0 = IERC20(_token0).balanceOf(address(this));
        balance1 = IERC20(_token1).balanceOf(address(this));

        _update(balance0, balance1);
        emit Burn(msg.sender, amount0, amount1, to);
    }

    /// @notice ROUTER-ONLY swap. This is what makes the 1% burn unavoidable:
    ///         arb bots cannot call the pool directly and skip the router.
    function swap(uint256 amount0Out, uint256 amount1Out, address to) external lock {
        require(msg.sender == ParityBurnFactory(factory).router(),
                "ParityBurn: ROUTER_ONLY");
        require(amount0Out > 0 || amount1Out > 0, "ParityBurn: INSUFFICIENT_OUTPUT");
        (uint112 _reserve0, uint112 _reserve1,) = getReserves();
        require(amount0Out < _reserve0 && amount1Out < _reserve1,
                "ParityBurn: INSUFFICIENT_LIQUIDITY");

        uint256 balance0;
        uint256 balance1;
        { // scope for _token{0,1} — avoids stack-too-deep (same trick as V2)
            address _token0 = token0;
            address _token1 = token1;
            require(to != _token0 && to != _token1, "ParityBurn: INVALID_TO");
            if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out);
            if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out);
            balance0 = IERC20(_token0).balanceOf(address(this));
            balance1 = IERC20(_token1).balanceOf(address(this));
        }
        uint256 amount0In = balance0 > _reserve0 - amount0Out
            ? balance0 - (_reserve0 - amount0Out) : 0;
        uint256 amount1In = balance1 > _reserve1 - amount1Out
            ? balance1 - (_reserve1 - amount1Out) : 0;
        require(amount0In > 0 || amount1In > 0, "ParityBurn: INSUFFICIENT_INPUT");
        { // scope for adjusted balances
            // 0.30% LP fee — adjusted-K invariant, identical maths to Uniswap V2
            uint256 balance0Adjusted = balance0 * 1000 - amount0In * 3;
            uint256 balance1Adjusted = balance1 * 1000 - amount1In * 3;
            require(balance0Adjusted * balance1Adjusted
                    >= uint256(_reserve0) * uint256(_reserve1) * 1000_000,
                    "ParityBurn: K");
        }
        _update(balance0, balance1);
        emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to);
    }

    /// @notice Send any balance above reserves to `to`.
    function skim(address to) external lock {
        address _token0 = token0;
        address _token1 = token1;
        _safeTransfer(_token0, to, IERC20(_token0).balanceOf(address(this)) - reserve0);
        _safeTransfer(_token1, to, IERC20(_token1).balanceOf(address(this)) - reserve1);
    }

    /// @notice Force reserves to match balances.
    function sync() external lock {
        _update(IERC20(token0).balanceOf(address(this)),
                IERC20(token1).balanceOf(address(this)));
    }

    // ── Helpers ──

    function _safeTransfer(address token, address to, uint256 value) private {
        (bool ok, bytes memory data) =
            token.call(abi.encodeWithSelector(IERC20.transfer.selector, to, value));
        require(ok && (data.length == 0 || abi.decode(data, (bool))),
                "ParityBurn: TRANSFER_FAILED");
    }

    function _min(uint256 x, uint256 y) private pure returns (uint256) {
        return x < y ? x : y;
    }

    function _sqrt(uint256 y) private pure returns (uint256 z) {
        if (y > 3) {
            z = y;
            uint256 x = y / 2 + 1;
            while (x < z) {
                z = x;
                x = (y / x + x) / 2;
            }
        } else if (y != 0) {
            z = 1;
        }
    }
}

// ════════════════════════════════════════════════════════════════════════
//  ParityBurnRouter — the only allowed way to swap on our pools.
//  Burns 1% of a designated burn token whenever it is a swap endpoint.
//  Deployed automatically by ParityBurnFactory (no constructor args).
// ════════════════════════════════════════════════════════════════════════

contract ParityBurnRouter {

    address public immutable factory; // the ParityBurnFactory that deployed us

    address public constant WETH = 0xA1077a294dDE1B09bB078844df40758a5D0f9a27; // WPLS

    // Designated burn tokens — 1% burned whenever one is a swap endpoint.
    // Ethereum addresses of the tokens forked onto PulseChain (+ DRK).
    address public constant BURN_PDAI  = 0x6B175474E89094C44Da98b954EedeAC495271d0F; // pDAI
    address public constant BURN_PWBTC = 0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599; // pWBTC
    address public constant BURN_PUSDT = 0xdAC17F958D2ee523a2206206994597C13D831ec7; // pUSDT
    address public constant BURN_PUSDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; // pUSDC
    address public constant BURN_PPAXG = 0x45804880De22913dAFE09f4980848ECE6EcbAf78; // pPAXG
    address public constant BURN_PSHIB = 0x95aD61b0a150d79219dCF64E1E6Cc01f0B64C4cE; // pSHIB
    address public constant BURN_PPEPE = 0x6982508145454Ce325dDbE47a25d4ec3d2311933; // pPEPE
    address public constant BURN_PBNB  = 0xB8c77482e45F1F44dE1745F52C74426C631bDD52; // pBNB
    address public constant BURN_PENS  = 0xC18360217D8F7Ab5e7c516566761Ea12Ce7F9D72; // pENS
    address public constant BURN_PCOMP = 0xc00e94Cb662C3520282E6f5717214004A7f26888; // pCOMP
    address public constant BURN_PAAVE = 0x7Fc66500c84A76Ad7e9c93437bFc5Ac33E2DDaE9; // pAAVE
    address public constant BURN_DRK   = 0x42B99ce10a94EfC6018092AD0A346c606be450C1; // DRK

    address public constant BURN_ADDRESS = 0x000000000000000000000000000000000000dEaD;
    uint256 public constant BURN_DIVISOR = 100; // 1% = amount / 100

    // Mutable burn-token registry. Seeded with the constants above in the
    // constructor; the owner (factory.owner) can add/remove tokens later via
    // setBurnToken() — so new burn pairs need no router redeploy.
    mapping(address => bool) private _burnToken;

    event Burned(address indexed token, uint256 amount);
    event BurnTokenSet(address indexed token, bool enabled);
    event Swapped(address indexed sender, address indexed tokenIn, address indexed tokenOut,
                  uint256 amountIn, uint256 amountOut);

    error Expired();
    error InvalidPath();
    error InsufficientOutput();
    error PairNotFound();

    modifier ensure(uint256 deadline) {
        if (block.timestamp > deadline) revert Expired();
        _;
    }

    /// @dev Standard caller authorization for the swap entrypoints.
    modifier auth() {
        require(ParityBurnFactory(factory).authed(msg.sender),
                "ParityBurn: FORBIDDEN");
        _;
    }

    constructor() {
        factory = msg.sender; // deployed by the factory constructor
        // Seed the initial burn tokens (the hardcoded set above).
        _burnToken[BURN_PDAI]  = true;
        _burnToken[BURN_PWBTC] = true;
        _burnToken[BURN_PUSDT] = true;
        _burnToken[BURN_PUSDC] = true;
        _burnToken[BURN_PPAXG] = true;
        _burnToken[BURN_PSHIB] = true;
        _burnToken[BURN_PPEPE] = true;
        _burnToken[BURN_PBNB]  = true;
        _burnToken[BURN_PENS]  = true;
        _burnToken[BURN_PCOMP] = true;
        _burnToken[BURN_PAAVE] = true;
        _burnToken[BURN_DRK]   = true;
    }

    /// @dev Only accept native coin from the WPLS contract during unwrap.
    receive() external payable {
        require(msg.sender == WETH, "ParityBurn: reject direct native");
    }

    // ── Burn helpers ──

    /// @notice Whether 1% of `token` is burned when it is a swap endpoint.
    function isBurnToken(address token) public view returns (bool) {
        return _burnToken[token];
    }

    /// @notice Owner-only: add (on=true) or remove (on=false) a burn token, so
    ///         custom pairs can burn 1% without redeploying the router.
    function setBurnToken(address token, bool on) external {
        require(msg.sender == ParityBurnFactory(factory).owner(), "ParityBurn: FORBIDDEN");
        _burnToken[token] = on;
        emit BurnTokenSet(token, on);
    }

    function _applyBurn(address token, uint256 amount) internal returns (uint256 remaining) {
        remaining = amount;
        if (isBurnToken(token)) {
            uint256 fee = amount / BURN_DIVISOR;
            if (fee > 0) {
                _safeTransfer(token, BURN_ADDRESS, fee);
                remaining = amount - fee;
                emit Burned(token, fee);
            }
        }
    }

    // ── Swaps ──

    function swapExactTokensForTokens(
        uint256 amountIn,
        uint256 amountOutMin,
        address[] calldata path,
        address to,
        uint256 deadline
    ) external ensure(deadline) auth returns (uint256 amountOut) {
        if (path.length < 2) revert InvalidPath();
        _safeTransferFrom(path[0], msg.sender, address(this), amountIn);
        amountOut = _executeSwap(amountIn, amountOutMin, path, to, false);
        emit Swapped(msg.sender, path[0], path[path.length - 1], amountIn, amountOut);
    }

    function swapExactETHForTokens(
        uint256 amountOutMin,
        address[] calldata path,
        address to,
        uint256 deadline
    ) external payable ensure(deadline) auth returns (uint256 amountOut) {
        if (path.length < 2 || path[0] != WETH) revert InvalidPath();
        IWETH(WETH).deposit{value: msg.value}();
        amountOut = _executeSwap(msg.value, amountOutMin, path, to, false);
        emit Swapped(msg.sender, path[0], path[path.length - 1], msg.value, amountOut);
    }

    function swapExactTokensForETH(
        uint256 amountIn,
        uint256 amountOutMin,
        address[] calldata path,
        address to,
        uint256 deadline
    ) external ensure(deadline) auth returns (uint256 amountOut) {
        if (path.length < 2 || path[path.length - 1] != WETH) revert InvalidPath();
        _safeTransferFrom(path[0], msg.sender, address(this), amountIn);
        amountOut = _executeSwap(amountIn, amountOutMin, path, to, true);
        emit Swapped(msg.sender, path[0], path[path.length - 1], amountIn, amountOut);
    }

    /// @dev Core swap: input tokens already held by this router.
    function _executeSwap(
        uint256 amountIn,
        uint256 amountOutMin,
        address[] calldata path,
        address to,
        bool outputETH
    ) internal returns (uint256 netOut) {
        address tokenIn = path[0];
        address tokenOut = path[path.length - 1];

        uint256 swapIn = _applyBurn(tokenIn, amountIn);
        uint256[] memory amounts = _getAmountsOut(swapIn, path);

        bool burnOut = isBurnToken(tokenOut);
        address finalTo = (burnOut || outputETH) ? address(this) : to;

        _safeTransfer(tokenIn, _pairFor(path[0], path[1]), amounts[0]);
        _swap(amounts, path, finalTo);

        netOut = amounts[amounts.length - 1];
        if (burnOut) netOut = _applyBurn(tokenOut, netOut);
        if (netOut < amountOutMin) revert InsufficientOutput();

        if (outputETH) {
            IWETH(WETH).withdraw(netOut);
            _safeTransferETH(to, netOut);
        } else if (burnOut) {
            _safeTransfer(tokenOut, to, netOut);
        }
    }

    function _swap(uint256[] memory amounts, address[] calldata path, address _to) internal {
        for (uint256 i; i < path.length - 1; i++) {
            (address input, address output) = (path[i], path[i + 1]);
            (address token0,) = _sortTokens(input, output);
            uint256 amountOut = amounts[i + 1];
            (uint256 amount0Out, uint256 amount1Out) =
                input == token0 ? (uint256(0), amountOut) : (amountOut, uint256(0));
            address to = i < path.length - 2 ? _pairFor(output, path[i + 2]) : _to;
            ParityBurnPair(_pairFor(input, output)).swap(amount0Out, amount1Out, to);
        }
    }

    // ── Liquidity (no burn applied — mirrors the hook, which only burns swaps) ──

    function addLiquidity(
        address tokenA,
        address tokenB,
        uint256 amountADesired,
        uint256 amountBDesired,
        uint256 amountAMin,
        uint256 amountBMin,
        address to,
        uint256 deadline
    ) external ensure(deadline) returns (uint256 amountA, uint256 amountB, uint256 liquidity) {
        (amountA, amountB) = _addLiquidity(tokenA, tokenB, amountADesired, amountBDesired,
                                           amountAMin, amountBMin);
        address pair = _pairFor(tokenA, tokenB);
        _safeTransferFrom(tokenA, msg.sender, pair, amountA);
        _safeTransferFrom(tokenB, msg.sender, pair, amountB);
        liquidity = ParityBurnPair(pair).mint(to);
    }

    function addLiquidityETH(
        address token,
        uint256 amountTokenDesired,
        uint256 amountTokenMin,
        uint256 amountETHMin,
        address to,
        uint256 deadline
    ) external payable ensure(deadline)
        returns (uint256 amountToken, uint256 amountETH, uint256 liquidity)
    {
        (amountToken, amountETH) = _addLiquidity(token, WETH, amountTokenDesired, msg.value,
                                                 amountTokenMin, amountETHMin);
        address pair = _pairFor(token, WETH);
        _safeTransferFrom(token, msg.sender, pair, amountToken);
        IWETH(WETH).deposit{value: amountETH}();
        IWETH(WETH).transfer(pair, amountETH);
        liquidity = ParityBurnPair(pair).mint(to);
        if (msg.value > amountETH) _safeTransferETH(msg.sender, msg.value - amountETH);
    }

    function removeLiquidity(
        address tokenA,
        address tokenB,
        uint256 liquidity,
        uint256 amountAMin,
        uint256 amountBMin,
        address to,
        uint256 deadline
    ) public ensure(deadline) returns (uint256 amountA, uint256 amountB) {
        address pair = _pairFor(tokenA, tokenB);
        ParityBurnPair(pair).transferFrom(msg.sender, pair, liquidity);
        (uint256 amount0, uint256 amount1) = ParityBurnPair(pair).burn(to);
        (address token0,) = _sortTokens(tokenA, tokenB);
        (amountA, amountB) = tokenA == token0 ? (amount0, amount1) : (amount1, amount0);
        require(amountA >= amountAMin, "ParityBurn: INSUFFICIENT_A");
        require(amountB >= amountBMin, "ParityBurn: INSUFFICIENT_B");
    }

    function removeLiquidityETH(
        address token,
        uint256 liquidity,
        uint256 amountTokenMin,
        uint256 amountETHMin,
        address to,
        uint256 deadline
    ) external ensure(deadline) returns (uint256 amountToken, uint256 amountETH) {
        (amountToken, amountETH) = removeLiquidity(token, WETH, liquidity, amountTokenMin,
                                                   amountETHMin, address(this), deadline);
        _safeTransfer(token, to, amountToken);
        IWETH(WETH).withdraw(amountETH);
        _safeTransferETH(to, amountETH);
    }

    function _addLiquidity(
        address tokenA,
        address tokenB,
        uint256 amountADesired,
        uint256 amountBDesired,
        uint256 amountAMin,
        uint256 amountBMin
    ) internal returns (uint256 amountA, uint256 amountB) {
        if (ParityBurnFactory(factory).getPair(tokenA, tokenB) == address(0)) {
            ParityBurnFactory(factory).createPair(tokenA, tokenB);
        }
        (uint256 reserveA, uint256 reserveB) = _getReserves(tokenA, tokenB);
        if (reserveA == 0 && reserveB == 0) {
            (amountA, amountB) = (amountADesired, amountBDesired);
        } else {
            uint256 amountBOptimal = _quote(amountADesired, reserveA, reserveB);
            if (amountBOptimal <= amountBDesired) {
                require(amountBOptimal >= amountBMin, "ParityBurn: INSUFFICIENT_B");
                (amountA, amountB) = (amountADesired, amountBOptimal);
            } else {
                uint256 amountAOptimal = _quote(amountBDesired, reserveB, reserveA);
                assert(amountAOptimal <= amountADesired);
                require(amountAOptimal >= amountAMin, "ParityBurn: INSUFFICIENT_A");
                (amountA, amountB) = (amountAOptimal, amountBDesired);
            }
        }
    }

    // ── Views / library maths ──

    function getAmountsOut(uint256 amountIn, address[] calldata path)
        external view returns (uint256[] memory amounts)
    {
        return _getAmountsOut(amountIn, path);
    }

    /// @notice Net amounts including the 1% burn on burn-token endpoints.
    function quoteWithBurn(uint256 amountIn, address[] calldata path)
        external view
        returns (uint256 amountInAfterBurn, uint256 grossOut, uint256 netOut)
    {
        amountInAfterBurn = amountIn;
        if (isBurnToken(path[0])) {
            amountInAfterBurn = amountIn - (amountIn / BURN_DIVISOR);
        }
        uint256[] memory amounts = _getAmountsOut(amountInAfterBurn, path);
        grossOut = amounts[amounts.length - 1];
        netOut = grossOut;
        if (isBurnToken(path[path.length - 1])) {
            netOut = grossOut - (grossOut / BURN_DIVISOR);
        }
    }

    function _getAmountsOut(uint256 amountIn, address[] calldata path)
        internal view returns (uint256[] memory amounts)
    {
        if (path.length < 2) revert InvalidPath();
        amounts = new uint256[](path.length);
        amounts[0] = amountIn;
        for (uint256 i; i < path.length - 1; i++) {
            (uint256 reserveIn, uint256 reserveOut) = _getReserves(path[i], path[i + 1]);
            amounts[i + 1] = _getAmountOut(amounts[i], reserveIn, reserveOut);
        }
    }

    function _getReserves(address tokenA, address tokenB)
        internal view returns (uint256 reserveA, uint256 reserveB)
    {
        (address token0,) = _sortTokens(tokenA, tokenB);
        (uint112 r0, uint112 r1,) = ParityBurnPair(_pairFor(tokenA, tokenB)).getReserves();
        (reserveA, reserveB) = tokenA == token0
            ? (uint256(r0), uint256(r1)) : (uint256(r1), uint256(r0));
    }

    /// @dev 0.30% LP fee (997/1000).
    function _getAmountOut(uint256 amountIn, uint256 reserveIn, uint256 reserveOut)
        internal pure returns (uint256 amountOut)
    {
        require(amountIn > 0, "ParityBurn: INSUFFICIENT_INPUT");
        require(reserveIn > 0 && reserveOut > 0, "ParityBurn: INSUFFICIENT_LIQUIDITY");
        uint256 amountInWithFee = amountIn * 997;
        uint256 numerator = amountInWithFee * reserveOut;
        uint256 denominator = reserveIn * 1000 + amountInWithFee;
        amountOut = numerator / denominator;
    }

    function _quote(uint256 amountA, uint256 reserveA, uint256 reserveB)
        internal pure returns (uint256 amountB)
    {
        require(amountA > 0, "ParityBurn: INSUFFICIENT_AMOUNT");
        require(reserveA > 0 && reserveB > 0, "ParityBurn: INSUFFICIENT_LIQUIDITY");
        amountB = (amountA * reserveB) / reserveA;
    }

    function _sortTokens(address tokenA, address tokenB)
        internal pure returns (address token0, address token1)
    {
        require(tokenA != tokenB, "ParityBurn: IDENTICAL_ADDRESSES");
        (token0, token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
        require(token0 != address(0), "ParityBurn: ZERO_ADDRESS");
    }

    function _pairFor(address tokenA, address tokenB) internal view returns (address pair) {
        pair = ParityBurnFactory(factory).getPair(tokenA, tokenB);
        if (pair == address(0)) revert PairNotFound();
    }

    // ── Safe transfer helpers ──

    function _safeTransfer(address token, address to, uint256 value) internal {
        (bool ok, bytes memory data) =
            token.call(abi.encodeWithSelector(IERC20.transfer.selector, to, value));
        require(ok && (data.length == 0 || abi.decode(data, (bool))),
                "ParityBurn: TRANSFER_FAILED");
    }

    function _safeTransferFrom(address token, address from, address to, uint256 value) internal {
        (bool ok, bytes memory data) =
            token.call(abi.encodeWithSelector(IERC20.transferFrom.selector, from, to, value));
        require(ok && (data.length == 0 || abi.decode(data, (bool))),
                "ParityBurn: TRANSFER_FROM_FAILED");
    }

    function _safeTransferETH(address to, uint256 value) internal {
        (bool ok,) = to.call{value: value}(new bytes(0));
        require(ok, "ParityBurn: ETH_TRANSFER_FAILED");
    }
}

// ════════════════════════════════════════════════════════════════════════
//  ParityBurnFactory — deploy THIS one contract; it deploys the router.
// ════════════════════════════════════════════════════════════════════════

contract ParityBurnFactory {

    address public owner;
    address public router;

    // ── Access control ──
    // Ward-based authorization, managed by `owner` (transfers with setOwner).
    // Independent of liquidity: adding/removing liquidity is always open and
    // anyone can withdraw 100% of their tokens.
    bool public restricted;
    mapping(address => uint256) public wards;

    mapping(address => mapping(address => address)) public getPair;
    address[] public allPairs;

    event PairCreated(address indexed token0, address indexed token1, address pair, uint256 count);
    event RouterSet(address indexed router);
    event OwnerSet(address indexed owner);
    event Rely(address indexed usr);
    event Deny(address indexed usr);
    event RestrictedSet(bool restricted);

    constructor() {
        owner = msg.sender;
        wards[msg.sender] = 1;   // deployer authorized
        restricted = true;       // authorization enforced by default
        emit Rely(msg.sender);
        // Deploy the router in the same tx — router records us as its factory.
        router = address(new ParityBurnRouter());
        emit RouterSet(router);
    }

    function allPairsLength() external view returns (uint256) {
        return allPairs.length;
    }

    /// @notice Point pools at an upgraded router (owner only). Pools check
    ///         this address live on every swap, so it takes effect instantly.
    function setRouter(address _router) external {
        require(msg.sender == owner, "ParityBurn: FORBIDDEN");
        require(_router != address(0), "ParityBurn: ZERO_ADDRESS");
        router = _router;
        emit RouterSet(_router);
    }

    function setOwner(address _owner) external {
        require(msg.sender == owner, "ParityBurn: FORBIDDEN");
        owner = _owner;
        emit OwnerSet(_owner);
    }

    // ── Access control ──

    /// @notice Grant `usr` authorization.
    function rely(address usr) external {
        require(msg.sender == owner, "ParityBurn: FORBIDDEN");
        wards[usr] = 1;
        emit Rely(usr);
    }

    /// @notice Revoke `usr`'s authorization.
    function deny(address usr) external {
        require(msg.sender == owner, "ParityBurn: FORBIDDEN");
        wards[usr] = 0;
        emit Deny(usr);
    }

    /// @notice Toggle whether authorization is enforced.
    function setRestricted(bool _restricted) external {
        require(msg.sender == owner, "ParityBurn: FORBIDDEN");
        restricted = _restricted;
        emit RestrictedSet(_restricted);
    }

    /// @notice Whether `usr` is authorized (or enforcement is off).
    function authed(address usr) external view returns (bool) {
        return !restricted || wards[usr] == 1;
    }

    /// @notice Create the pool for a token pair. Open access (the router
    ///         calls this automatically on first liquidity add).
    function createPair(address tokenA, address tokenB) external returns (address pair) {
        require(tokenA != tokenB, "ParityBurn: IDENTICAL_ADDRESSES");
        (address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
        require(token0 != address(0), "ParityBurn: ZERO_ADDRESS");
        require(getPair[token0][token1] == address(0), "ParityBurn: PAIR_EXISTS");

        pair = address(new ParityBurnPair{salt: keccak256(abi.encodePacked(token0, token1))}());
        ParityBurnPair(pair).initialize(token0, token1);

        getPair[token0][token1] = pair;
        getPair[token1][token0] = pair;
        allPairs.push(pair);
        emit PairCreated(token0, token1, pair, allPairs.length);
    }
}