Address
0x5cc05ecb24fc3dbae9a7ea9083e2f4fe6d2f5495Current 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);
}
}