Skip to main content
PulseScanner.io

Address

0x5cc05ecb24fc3dbae9a7ea9083e2f4fe6d2f5495
Current Holdings
$9.04
TXs sent
0
First Active
2026-08-10
block 27,252,672
Last Active
today
block 27,552,613
Funded By
not identified

Net worth historyi

39 snapshots · to block 27,552,613coverage change 26 Augcoverage change 27 Augcoverage change 27 Augcoverage change 27 Augcoverage change 27 Aug
exact matchPLSXPenaltyBurnersolc 0.8.26+commit.8a97fa7aruntime exact · creation exact
// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;

import {ReentrancyGuard} from "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v5.0.2/contracts/utils/ReentrancyGuard.sol";
import {Ownable2Step, Ownable} from "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v5.0.2/contracts/access/Ownable2Step.sol";
import {IERC20} from "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v5.0.2/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v5.0.2/contracts/token/ERC20/utils/SafeERC20.sol";

interface IPulseXRouter {
    function factory() external pure returns (address);
    function swapExactTokensForTokens(
        uint256 amountIn,
        uint256 amountOutMin,
        address[] calldata path,
        address to,
        uint256 deadline
    ) external returns (uint256[] memory amounts);
}

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

interface IPulseXPair {
    function getReserves() external view returns (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast);
    function token0() external view returns (address);
}

/**
 * @title  PLSXPenaltyBurner
 * @notice PLSX-funded sibling of MoreBurnerV2. Same drip mechanics — 1%/day,
 *         144 ten-minute intervals, 1.5% caller incentive, bought MORE sent
 *         to the dead address since MORE has no burn() function.
 * @dev    DIFFERENCES FROM MoreBurnerV2:
 *         - Funding asset is PLSX (ERC20), not native PLS. PLSXStaking's
 *           unstake() sends PLSX here via a plain transfer, so there's no
 *           receive()/distributeETHForBurning() hook — the "pool" is just
 *           this contract's live PLSX balance, read directly each interval
 *           instead of tracked via a totalETHBurn counter.
 *         - Swap is a single 2-hop router call, path [PLSX, WPLS, MORE],
 *           instead of MoreBurnerV2's IWPLS.deposit() + [WPLS, MORE].
 *         - The 1.5% caller incentive is paid in PLSX, not native PLS.
 *         - STACKING: unlike the earlier MoreBurnerV2 revisions (where
 *           missedIntervals only ever advanced bookkeeping, never scaled the
 *           payout), this matches the current MoreBurnerV2: intervalsToBurn
 *           is the gap between the current interval number and the last one
 *           actually paid out, multiplying the payout accordingly. No cap —
 *           same as the source — so a long-idle burner pays out
 *           proportionally more on the next call, up to the full pool.
 *         - Price-drift guard is unchanged: still compares MORE/WPLS spot
 *           price (the leg that actually determines the burn ratio) across
 *           calls. The PLSX/WPLS leg is not separately guarded, same as
 *           MoreBurnerV2 not guarding its native-PLS input.
 *
 *         Router / WPLS addresses, dead address, and the price-drift
 *         mechanism are otherwise carried over unchanged.
 *
 * @dev    ⚠ COMPILE WITH EVM VERSION SET TO "shanghai", NOT THE DEFAULT.
 *         Solidity 0.8.24+ defaults its bytecode target to "cancun", which
 *         emits the MCOPY opcode (used e.g. when building the in-memory
 *         swap `path` array). PulseChain's execution client does not
 *         support Cancun yet — a contract compiled with the default target
 *         deploys successfully but reverts with "invalid opcode: MCOPY" on
 *         every call that touches the affected code, discovered the hard
 *         way debugging this exact contract. In Remix: Solidity Compiler
 *         tab → Advanced Configurations → EVM VERSION → shanghai, BEFORE
 *         compiling and deploying.
 */
contract PLSXPenaltyBurner is Ownable2Step, ReentrancyGuard {
    using SafeERC20 for IERC20;

    IPulseXRouter public immutable ROUTER;
    address public immutable WPLS_ADDRESS;
    address public immutable MORE_ADDRESS;
    IERC20 public immutable PLSX;
    address public constant DEAD_ADDRESS = 0x000000000000000000000000000000000000dEaD;

    uint32 public immutable startTimeStamp;
    uint32 public lastBurnedPLSXIntervalStartTimestamp;
    uint32 public lastPLSXBurnIntervalNumber;
    uint32 public lastBurnedIntervalNumber = type(uint32).max; // sentinel: nothing burned yet

    uint256 public totalMoreBurnt;
    uint256 public totalPlsxUsedForBurns;

    uint256 public constant INTERVAL_TIME = 10 minutes;
    uint256 public constant INTERVALS_PER_DAY = 144;
    uint256 public constant BPS_DENOM = 10_000;
    uint256 public constant INCENTIVE_FEE = 150; // 1.5%
    uint256 public dailyAllocation = 100; // 1%, owner-tunable

    address public morePlsPair;
    uint256 public lastAcceptedPrice; // MORE-per-WPLS, scaled 1e18
    uint16 public maxPriceDriftBps = 1000; // 10%, owner-tunable 200-10000

    event MoreBoughtAndBurned(uint256 indexed plsxAmount, uint256 indexed moreBurnt, address indexed caller);
    event DailyAllocationUpdated(uint256 newDailyAllocation);
    event MaxPriceDriftUpdated(uint16 newMaxDriftBps);

    error InvalidInput();
    error NothingToDistribute();
    error IntervalAlreadyBurned();
    error PriceMovedTooMuch();

    constructor(
        address _plsx,
        address _more,
        address _router,
        address _wpls,
        uint32 _startTimestamp,
        address _owner
    ) Ownable(_owner) {
        if (_plsx == address(0) || _more == address(0) || _router == address(0) || _wpls == address(0)) {
            revert InvalidInput();
        }
        PLSX = IERC20(_plsx);
        MORE_ADDRESS = _more;
        ROUTER = IPulseXRouter(_router);
        WPLS_ADDRESS = _wpls;

        uint32 start = _startTimestamp == 0 ? uint32(block.timestamp) : _startTimestamp;
        if (start > block.timestamp + 30 days) revert InvalidInput();
        startTimeStamp = start;
        lastBurnedPLSXIntervalStartTimestamp = start;

        PLSX.approve(_router, type(uint256).max);
    }

    modifier burnIntervalUpdate() {
        if (block.timestamp - lastBurnedPLSXIntervalStartTimestamp > INTERVAL_TIME) {
            uint256 missedIntervals = (block.timestamp - lastBurnedPLSXIntervalStartTimestamp) / INTERVAL_TIME;
            lastBurnedPLSXIntervalStartTimestamp += uint32(missedIntervals * INTERVAL_TIME);
            lastPLSXBurnIntervalNumber += uint32(missedIntervals);
        }
        _;
    }

    /// @dev Auto-resolves the MORE/WPLS pair the first time it exists —
    ///      someone needs to have created and seeded it separately.
    ///      Returns 0 before that, treated as "nothing to compare yet."
    function _spotPrice() private returns (uint256) {
        if (morePlsPair == address(0)) {
            morePlsPair = IPulseXFactory(ROUTER.factory()).getPair(MORE_ADDRESS, WPLS_ADDRESS);
            if (morePlsPair == address(0)) return 0;
        }
        (uint112 r0, uint112 r1, ) = IPulseXPair(morePlsPair).getReserves();
        bool moreIsToken0 = IPulseXPair(morePlsPair).token0() == MORE_ADDRESS;
        (uint256 moreReserve, uint256 plsReserve) = moreIsToken0 ? (uint256(r0), uint256(r1)) : (uint256(r1), uint256(r0));
        if (plsReserve == 0) return 0;
        return (moreReserve * 1e18) / plsReserve;
    }

    /// @notice Buys $MORE with the accumulated PLSX allocation and sends it
    ///         to the dead address. Permissionless; caller earns 1.5% in PLSX.
    /// @dev STACKING: matches MoreBurnerV2's actual mechanism — intervalsToBurn
    ///      is the gap between the current interval number (already advanced
    ///      by burnIntervalUpdate) and the last interval that was actually
    ///      paid out, computed BEFORE lastBurnedIntervalNumber is overwritten.
    ///      No cap, same as the source — a long-idle burner pays out
    ///      proportionally more on the next call, up to the full pool balance.
    function swapPlsxForMoreAndBurn(uint256 _deadline) external nonReentrant burnIntervalUpdate {
        // Same guard as MoreBurnerV2 — without it, this could be called
        // repeatedly within one 10-minute window, bypassing the drip rate.
        if (lastBurnedIntervalNumber == lastPLSXBurnIntervalNumber) revert IntervalAlreadyBurned();

        uint256 intervalsToBurn = lastBurnedIntervalNumber == type(uint32).max
            ? 1
            : lastPLSXBurnIntervalNumber - lastBurnedIntervalNumber;
        lastBurnedIntervalNumber = lastPLSXBurnIntervalNumber;

        uint256 plsxPool = PLSX.balanceOf(address(this));
        uint256 dailyAmount = (plsxPool * dailyAllocation) / BPS_DENOM;
        uint256 amountAllocated = (dailyAmount / INTERVALS_PER_DAY) * intervalsToBurn;
        if (amountAllocated > plsxPool) amountAllocated = plsxPool;
        if (amountAllocated == 0) revert NothingToDistribute();

        uint256 incentive = (amountAllocated * INCENTIVE_FEE) / BPS_DENOM;
        uint256 swapAmount = amountAllocated - incentive;

        uint256 priceBefore = _spotPrice();
        if (priceBefore > 0 && lastAcceptedPrice > 0) {
            uint256 diff = priceBefore > lastAcceptedPrice ? priceBefore - lastAcceptedPrice : lastAcceptedPrice - priceBefore;
            if ((diff * BPS_DENOM) / lastAcceptedPrice > maxPriceDriftBps) revert PriceMovedTooMuch();
        }

        address[] memory path = new address[](3);
        path[0] = address(PLSX);
        path[1] = WPLS_ADDRESS;
        path[2] = MORE_ADDRESS;

        uint256 balBefore = IERC20(MORE_ADDRESS).balanceOf(address(this));
        ROUTER.swapExactTokensForTokens(swapAmount, 0, path, address(this), _deadline);
        uint256 moreAmount = IERC20(MORE_ADDRESS).balanceOf(address(this)) - balBefore;

        totalMoreBurnt += moreAmount;
        totalPlsxUsedForBurns += swapAmount;
        lastAcceptedPrice = _spotPrice();

        // Sweeps the FULL current MORE balance, not just this swap's output —
        // same stuck-token protection as MoreBurnerV2. Any stray MORE sitting
        // here (dust, an accidental direct transfer) gets sent along too
        // instead of staying stuck forever.
        uint256 fullBalance = IERC20(MORE_ADDRESS).balanceOf(address(this));
        IERC20(MORE_ADDRESS).safeTransfer(DEAD_ADDRESS, fullBalance);

        if (incentive > 0) {
            PLSX.safeTransfer(msg.sender, incentive);
        }

        emit MoreBoughtAndBurned(swapAmount, moreAmount, msg.sender);
    }

    function setDailyAllocation(uint256 _newDailyAllocation) external onlyOwner {
        if (_newDailyAllocation == 0 || _newDailyAllocation > 1000) revert InvalidInput();
        dailyAllocation = _newDailyAllocation;
        emit DailyAllocationUpdated(_newDailyAllocation);
    }

    function setMaxPriceDrift(uint16 _newMaxDriftBps) external onlyOwner {
        if (_newMaxDriftBps < 200 || _newMaxDriftBps > 10000) revert InvalidInput();
        maxPriceDriftBps = _newMaxDriftBps;
        emit MaxPriceDriftUpdated(_newMaxDriftBps);
    }
}