// SPDX-License-Identifier: MIT pragma solidity ^0.8.24; interface IUniswapV2Router { function WETH() external view returns (address); function getAmountsOut(uint amountIn, address[] calldata path) external view returns (uint[] memory); function swapExactETHForTokensSupportingFeeOnTransferTokens( uint amountOutMin, address[] calldata path, address to, uint deadline ) external payable; } interface IERC20Minimal { function balanceOf(address) external view returns (uint256); } /// @title BuybackBurn /// @notice Trustless buy-back-and-burn for the platform token. /// @dev No owner, no admin key, no withdraw. ETH sent here (e.g. launch fees) /// can ONLY ever leave by being swapped for TOKEN on Uniswap V2 and sent to /// the dead address. Anyone (a keeper/cron) may call buybackAndBurn(); the /// caller only pays gas. Because there is no way to extract the ETH except /// as burned TOKEN, there is no key worth stealing. /// /// SANDWICH HARDENING (2026-07-19, pre-audit finding resolved): the public /// entrypoint was drainable as MEV — pump the pool, call with minTokensOut=0, /// dump. Now each call spends at most `maxEthPerCall` and calls are separated /// by `cooldownSec`, both immutable. An attacker's extractable value per /// sandwich is bounded by the cap's price impact, and the drain RATE is /// bounded by the cooldown — while the contract stays ownerless. contract BuybackBurn { address public constant DEAD = 0x000000000000000000000000000000000000dEaD; address public immutable token; // the platform token to buy & burn IUniswapV2Router public immutable router; address public immutable weth; uint256 public immutable maxEthPerCall; // per-call spend ceiling (wei) uint256 public immutable cooldownSec; // min seconds between calls uint256 public totalEthSpent; // lifetime ETH used for buybacks uint256 public totalTokensBurned; // lifetime TOKEN sent to dead address uint64 public lastBuybackAt; // timestamp of the last successful call uint256 private _locked = 1; // minimal reentrancy guard event BuybackAndBurn(address indexed caller, uint256 ethIn, uint256 tokensBurned); modifier nonReentrant() { require(_locked == 1, "reentrant"); _locked = 2; _; _locked = 1; } constructor(address token_, address router_, uint256 maxEthPerCall_, uint256 cooldownSec_) { require(token_ != address(0) && router_ != address(0), "zero addr"); require(maxEthPerCall_ > 0, "zero cap"); token = token_; router = IUniswapV2Router(router_); weth = IUniswapV2Router(router_).WETH(); maxEthPerCall = maxEthPerCall_; cooldownSec = cooldownSec_; } /// @notice Accept ETH (launch fees, top-ups, anyone). receive() external payable {} /// @notice Quote the expected TOKEN out for the NEXT buyback call /// (capped spend, not the whole balance). Keepers read this to /// set a sane minTokensOut off-chain. function quote() external view returns (uint256 ethIn, uint256 tokensOut) { ethIn = _spendable(); if (ethIn == 0) return (0, 0); address[] memory path = _path(); uint256[] memory outs = router.getAmountsOut(ethIn, path); tokensOut = outs[outs.length - 1]; } /// @notice Swap up to maxEthPerCall of the balance for TOKEN and burn it. /// @param minTokensOut Slippage floor; a keeper computes this from quote(). /// Passing 0 disables slippage protection — only for tiny/test amounts. function buybackAndBurn(uint256 minTokensOut) external nonReentrant { require(block.timestamp >= uint256(lastBuybackAt) + cooldownSec, "cooldown"); uint256 ethIn = _spendable(); require(ethIn > 0, "no ETH"); lastBuybackAt = uint64(block.timestamp); // effects before interaction uint256 burnedBefore = IERC20Minimal(token).balanceOf(DEAD); router.swapExactETHForTokensSupportingFeeOnTransferTokens{value: ethIn}( minTokensOut, _path(), DEAD, // send bought tokens straight to the burn address block.timestamp ); uint256 burned = IERC20Minimal(token).balanceOf(DEAD) - burnedBefore; totalEthSpent += ethIn; totalTokensBurned += burned; emit BuybackAndBurn(msg.sender, ethIn, burned); } function _spendable() internal view returns (uint256) { uint256 bal = address(this).balance; return bal < maxEthPerCall ? bal : maxEthPerCall; } function _path() internal view returns (address[] memory path) { path = new address[](2); path[0] = weth; path[1] = token; } }