// SPDX-License-Identifier: MIT pragma solidity ^0.8.24; /// @notice Swaps native ETH for `token` and delivers the output to `to`. /// One adapter per trading venue (bonding curve, then the graduated pool). interface IBuyAdapter { function buy(address token, address to, uint256 minOut) external payable returns (uint256 bought); } interface IERC20View { function balanceOf(address account) external view returns (uint256); function totalSupply() external view returns (uint256); } /// @title CoulombEngine /// @notice Creator fees charge the capacitor. At 1 ETH, anyone can discharge it: /// the full 1 ETH buys $COULOMB in one swap and every token bought lands in the /// burn address inside the same transaction. /// /// What the owner cannot do: withdraw the capacitor, change the threshold, change /// the split, or swap in a new adapter without a public 48 hour notice. contract CoulombEngine { uint256 public constant THRESHOLD = 1 ether; uint256 public constant CAPACITOR_BPS = 9_700; uint256 public constant BPS = 10_000; uint256 public constant ADAPTER_DELAY = 48 hours; address public constant BURN = 0x000000000000000000000000000000000000dEaD; address public immutable token; address public immutable keeperWallet; address public owner; IBuyAdapter public adapter; address public pendingAdapter; uint256 public pendingAdapterAt; bool public paused; uint256 public capacitor; // wei waiting for the next discharge uint256 public keeperOwed; // wei owed to the keeper wallet (3% of fees) uint256 public totalCharged; // every wei of creator fees received uint256 public totalSpent; // wei spent on buybacks uint256 public totalBurned; // token units delivered to BURN by discharges uint256 public discharges; uint256 private _lock = 1; event Charged(address indexed from, uint256 amount, uint256 toCapacitor, uint256 toKeeper); event Discharged(uint256 indexed id, address indexed caller, uint256 ethIn, uint256 tokensBurned, uint256 capacitorAfter); event AdapterProposed(address indexed adapter, uint256 activeAt); event AdapterActivated(address indexed adapter); event PausedSet(bool paused); event KeeperPaid(uint256 amount); event OwnershipTransferred(address indexed from, address indexed to); error NotOwner(); error IsPaused(); error BelowThreshold(); error NothingBurned(); error SlippageExceeded(uint256 burned, uint256 minOut); error TooEarly(); error NoAdapter(); error Reentrancy(); error TransferFailed(); modifier onlyOwner() { if (msg.sender != owner) revert NotOwner(); _; } modifier nonReentrant() { if (_lock != 1) revert Reentrancy(); _lock = 2; _; _lock = 1; } constructor(address token_, address keeperWallet_, address adapter_) { token = token_; keeperWallet = keeperWallet_; adapter = IBuyAdapter(adapter_); owner = msg.sender; emit OwnershipTransferred(address(0), msg.sender); emit AdapterActivated(adapter_); } /// @notice Creator fees arrive here as plain ETH transfers. receive() external payable { uint256 toKeeper = (msg.value * (BPS - CAPACITOR_BPS)) / BPS; uint256 toCapacitor = msg.value - toKeeper; capacitor += toCapacitor; keeperOwed += toKeeper; totalCharged += msg.value; emit Charged(msg.sender, msg.value, toCapacitor, toKeeper); } /// @notice Fires one discharge. Callable by anyone once the capacitor holds 1 ETH. /// @param minOut Smallest token amount the burn address must receive. function discharge(uint256 minOut) external nonReentrant returns (uint256 burned) { if (paused) revert IsPaused(); if (capacitor < THRESHOLD) revert BelowThreshold(); if (address(adapter) == address(0)) revert NoAdapter(); capacitor -= THRESHOLD; uint256 before = IERC20View(token).balanceOf(BURN); adapter.buy{value: THRESHOLD}(token, BURN, minOut); // Count what the burn address received, not what the adapter reports. burned = IERC20View(token).balanceOf(BURN) - before; if (burned == 0) revert NothingBurned(); if (burned < minOut) revert SlippageExceeded(burned, minOut); totalSpent += THRESHOLD; totalBurned += burned; uint256 id = ++discharges; emit Discharged(id, msg.sender, THRESHOLD, burned, capacitor); } /// @notice Everything the site needs in one call. function state() external view returns ( uint256 capacitor_, uint256 discharges_, uint256 totalBurned_, uint256 totalCharged_, uint256 totalSpent_, uint256 keeperOwed_, bool paused_, address adapter_, address pendingAdapter_, uint256 pendingAdapterAt_ ) { return (capacitor, discharges, totalBurned, totalCharged, totalSpent, keeperOwed, paused, address(adapter), pendingAdapter, pendingAdapterAt); } function payKeeper() external nonReentrant { uint256 amount = keeperOwed; keeperOwed = 0; (bool ok,) = keeperWallet.call{value: amount}(""); if (!ok) revert TransferFailed(); emit KeeperPaid(amount); } function proposeAdapter(address next) external onlyOwner { pendingAdapter = next; pendingAdapterAt = block.timestamp + ADAPTER_DELAY; emit AdapterProposed(next, pendingAdapterAt); } function activateAdapter() external onlyOwner { if (pendingAdapter == address(0) || block.timestamp < pendingAdapterAt) revert TooEarly(); adapter = IBuyAdapter(pendingAdapter); pendingAdapter = address(0); pendingAdapterAt = 0; emit AdapterActivated(address(adapter)); } function setPaused(bool value) external onlyOwner { paused = value; emit PausedSet(value); } function transferOwnership(address next) external onlyOwner { emit OwnershipTransferred(owner, next); owner = next; } }