// SPDX-License-Identifier: MIT pragma solidity ^0.8.24; /// @notice The launch curve $CALC trades on. Bound at deploy; the engine only /// ever calls buy(), and only with tokens routed straight to BURN. interface ICurve { /// Buys `token` with msg.value and delivers it to `to`. /// Reverts unless at least `minOut` tokens arrive. function buy(address token, uint256 minOut, address to) external payable returns (uint256 amountOut); } /// @notice Where the launchpad holds creator fees until someone claims them. interface IFeeEscrow { /// Sends the creator fee accrued for `token` to its configured recipient /// (the FeeSplitter) and returns the amount sent. function claim(address token) external returns (uint256 amount); } interface IFeeSplitter { function split() external returns (uint256 toEngine, uint256 toDesk); } /// @title CalcEngine /// @notice Proof of work that burns. /// /// keccak256(seed ‖ miner ‖ nonce) ≤ target proves a block. The /// engine then spends one eighth of its ETH budget on $CALC and has /// the curve deliver every token to the dead address. The prover's /// address goes into the Block event; that log is the claim on the /// other half of the fee, which the desk pays in NVDA. /// /// Absent on purpose: an owner, approve, any transfer to a chosen /// address, a rescue function, delegatecall and selfdestruct. contract CalcEngine { address public constant BURN = 0x000000000000000000000000000000000000dEaD; uint256 public constant SLICE = 8; // each block spends budget / 8 uint256 public constant FAST = 300; // seconds; faster -> target * 3/4 uint256 public constant SLOW = 1200; // seconds; slower -> target * 4/3 uint256 public constant T_MIN = 1 << 200; // hardest the target gets uint256 public constant T_MAX = 1 << 240; // easiest the target gets address public immutable token; ICurve public immutable curve; IFeeEscrow public immutable escrow; IFeeSplitter public immutable splitter; bytes32 public seed; uint256 public target; uint64 public height; uint64 public lastBlockAt; event Block( uint64 indexed height, address indexed miner, bytes32 hash, uint256 ethSpent, uint256 burned, uint256 nextTarget ); event Harvest(address indexed caller, uint256 claimed, uint256 toEngine); error AboveTarget(bytes32 hash, uint256 target); error EmptyBudget(); error BadTarget(); constructor(address token_, ICurve curve_, IFeeEscrow escrow_, IFeeSplitter splitter_, uint256 genesisTarget) { if (genesisTarget < T_MIN || genesisTarget > T_MAX) revert BadTarget(); token = token_; curve = curve_; escrow = escrow_; splitter = splitter_; seed = keccak256("dotcalc genesis"); // the pool mines this seed today target = genesisTarget; // 2^236 at launch lastBlockAt = uint64(block.timestamp); } /// The budget arrives from the splitter. Nothing else can leave with it. receive() external payable {} /// @notice Prove a block. The nonce only works for msg.sender: the /// caller's address is part of the hashed bytes. /// @param minOut the least $CALC the purchase may deliver; the curve /// refuses zero, which stops a block from buying at any price. function mine(uint256 nonce, uint256 minOut) external returns (bytes32 h) { h = keccak256(abi.encodePacked(seed, msg.sender, nonce)); // 32 + 20 + 32 bytes if (uint256(h) > target) revert AboveTarget(h, target); uint256 spend = address(this).balance / SLICE; if (spend == 0) revert EmptyBudget(); // state moves before the external call: the seed is spent either way uint256 next = retarget(target, block.timestamp - lastBlockAt); seed = h; target = next; unchecked { height += 1; } lastBlockAt = uint64(block.timestamp); uint256 burned = curve.buy{value: spend}(token, minOut, BURN); emit Block(height, msg.sender, h, spend, burned, next); } /// @notice Pull creator fees out of escrow and through the splitter. /// Open to anyone, so the budget refills without us. function harvest() external returns (uint256 claimed, uint256 toEngine) { claimed = escrow.claim(token); (toEngine, ) = splitter.split(); emit Harvest(msg.sender, claimed, toEngine); } /// @notice The retarget rule, exposed so anyone can check a block log /// against it. Same arithmetic as the pool: integer, round down. function retarget(uint256 t, uint256 dt) public pure returns (uint256) { if (dt < FAST) t = (t * 3) / 4; else if (dt > SLOW) t = (t * 4) / 3; // t <= 2^240, so no overflow if (t < T_MIN) t = T_MIN; if (t > T_MAX) t = T_MAX; return t; } /// @notice Would `nonce` prove the current block for `miner`? function check(address miner, uint256 nonce) external view returns (bool ok, bytes32 h) { h = keccak256(abi.encodePacked(seed, miner, nonce)); ok = uint256(h) <= target; } /// @notice Expected hashes per block at the current target: 2^256 / (T + 1). function difficulty() external view returns (uint256) { return type(uint256).max / (target + 1); } }