// SPDX-License-Identifier: MIT pragma solidity 0.8.24; import {Ownable, Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol"; import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; import {IMining} from "./interfaces/IMining.sol"; import {PoolMath} from "./PoolMath.sol"; import {TapeoutEntitlementReceipt} from "./TapeoutEntitlementReceipt.sol"; /// @title TapeOut miner custody and BEM revenue distribution pool /// @notice Holds TapeOut miner NFTs and distributes the BEM it actually receives in the relative /// weights NAND + LATCH 100 : A 1.25 : B 1.25 : C 2.5, held exactly as 80 : 1 : 1 : 2 of 84 /// parts. The participants' part goes to the NAND and LATCH receipt collections, split by the /// aggregate NAND/LATCH burn of the admitted miners as read from the Mining contract; receipt /// holders claim per receipt ID and unclaimed income follows the ID. A and B are role /// recipients. C is the fixed sink address `SINK`: every release of A or B also transfers /// C's whole unpaid part there, as a plain BEM transfer (not a token burn). /// @dev Lifecycle: deploy (binds receipts, recipients and a fixed depositor whitelist) -> owner /// `activate` (receipt supplies finalized and fixed as denominators) -> whitelisted admissions, /// harvests, claims and releases. After activation the owner has no remaining power, so /// renouncing ownership does not affect operation. There is no miner withdrawal, BEM /// withdrawal, pause, upgrade, or arbitrary call path. /// /// Admission (`depositMiners`, `registerMiners`) is an atomic accounting boundary: /// 1. validate the batch and read each incoming miner's burn from Mining; /// 2. recognize BEM already held, under the old composition; /// 3. call `Mining.claim` for every registered miner and recognize it under the old composition /// (a failed claim reverts the whole admission, unless Mining reports the miner REVOKED); /// 4. checkpoint the old period, take custody, add the batch to the composition totals; /// 5. claim the incoming miners' carried income and recognize it under the new composition. contract TapeoutMinerPool is Ownable2Step, ReentrancyGuard { using SafeERC20 for IERC20; enum Category { NAND, LATCH } enum Recipient { A, B } struct Deployment { address owner; address bem; address mining; address minerCollection; address nandReceipt; address latchReceipt; address recipientA; address recipientB; address[] depositors; string poolName; } struct Miner { bytes32 key; uint32 nandBurn; uint32 latchBurn; uint64 admittedInPeriod; bool registered; } /// @notice bemBalance + deficit == sum(accrued - paid) + residue + unrecognized. struct Reconciliation { uint256 bemBalance; uint256 unrecognized; uint256 deficit; uint256 totalAccounted; uint256 totalPaid; uint256 recipientAAccrued; uint256 recipientAPaid; uint256 recipientBAccrued; uint256 recipientBPaid; uint256 sinkAccrued; uint256 sinkPaid; uint256 nandIncome; uint256 nandReceiptsAccrued; uint256 nandReceiptsPaid; uint256 latchIncome; uint256 latchReceiptsAccrued; uint256 latchReceiptsPaid; uint256 residue; } string public constant VERSION = "TapeoutMinerPool/2.1.0"; uint256 public constant TOTAL_PARTS = PoolMath.TOTAL_PARTS; uint256 public constant PARTICIPANT_PARTS = PoolMath.PARTICIPANT_PARTS; uint256 public constant RECIPIENT_PARTS = PoolMath.RECIPIENT_PARTS; uint256 public constant SINK_PARTS = PoolMath.SINK_PARTS; /// @notice C: the fixed destination of the sink part. Payments are ordinary BEM transfers to this /// address (they need no signature from it); nobody can change or redirect it, and it is /// not an A/B role. Whether BEM's totalSupply changes on such a transfer is BEM's behaviour. address public constant SINK = 0x000000000000000000000000000000000000dEaD; uint8 public constant MINER_ACTIVE = 1; /// @dev Mining status 2. Permanent: a revoked circuit can never mine again, and Mining refuses /// every claim for it (docs/VERIFICATION_REPORT.md, Mining lifecycle). uint8 public constant MINER_REVOKED = 2; uint256 public constant MAX_DEPOSITORS = 16; uint256 public constant MAX_ADMISSION_BATCH = 50; uint256 public constant MAX_PAGE = 500; bytes32 internal constant NAND_CATEGORY = keccak256("TAPEOUT_NAND_ENTITLEMENT"); bytes32 internal constant LATCH_CATEGORY = keccak256("TAPEOUT_LATCH_ENTITLEMENT"); IERC20 public immutable bem; IMining public immutable mining; IERC721 public immutable minerCollection; TapeoutEntitlementReceipt public immutable nandReceipt; TapeoutEntitlementReceipt public immutable latchReceipt; string public poolName; address public recipientA; address public recipientB; address[] private _depositors; mapping(address account => bool allowed) public isDepositor; bool public active; uint64 public activatedAt; uint256 public nandShares; uint256 public latchShares; uint256 public totalNandBurn; uint256 public totalLatchBurn; uint256 public compositionPeriod; uint256 public nandBase; uint256 public latchBase; uint256 public participantBase; uint256 public totalAccounted; uint256 public totalPaid; uint256 public recipientAPaid; uint256 public recipientBPaid; uint256 public sinkPaid; uint256[2] private _receiptsPaid; mapping(Category => mapping(uint256 id => uint256 amount)) private _claimed; uint256[] private _minerIds; bytes32[] private _minerKeys; mapping(uint256 tokenId => Miner) private _miners; error AlreadyActive(); error NotActive(); error InvalidAddress(); error InvalidName(); error InvalidDepositors(); error IncompatibleReceipt(); error IssuanceNotFinalized(); error UnexpectedShareCounts(uint256 nandShares, uint256 latchShares); error NoShares(); error NotDepositor(address caller); error InvalidBatch(); error DuplicateMiner(uint256 tokenId); error MinerAlreadyRegistered(uint256 tokenId); error MinerNotHeld(uint256 tokenId, address holder); error MinerNotActive(uint256 tokenId); error CompositionMismatch(uint256 tokenId); error EmptyComposition(uint256 tokenId); error UnsupportedCategory(Category category); error SettlementFailed(uint256 tokenId, bytes reason); error UnknownMiner(uint256 tokenId); error ReceiptIdOutOfRange(Category category, uint256 id); error NotHolder(Category category, uint256 id); error InvalidPage(); error Unauthorized(); error BalanceDecreased(uint256 balance, uint256 expected); error UnexpectedTokenBehavior(); event Deployed( address nandReceipt, address latchReceipt, address recipientA, address recipientB, address[] depositors ); event Activated(uint256 nandShares, uint256 latchShares, uint256 activatedAt); event IncomeRecognized(uint256 received, uint256 totalAccounted, uint256 compositionPeriod); event CompositionCheckpoint( uint256 indexed period, uint256 totalAccounted, uint256 nandBase, uint256 latchBase, uint256 participantBase ); event MinerRegistered( uint256 indexed tokenId, bytes32 indexed key, address indexed depositor, uint256 nandBurn, uint256 latchBurn ); event RevokedMinerSkipped(uint256 indexed tokenId); event MinersAdmitted( address indexed depositor, uint256 indexed period, uint256 count, uint256 settledOldIncome, uint256 carriedIncome, uint256 totalNandBurn, uint256 totalLatchBurn ); event MinerHarvested(uint256 indexed tokenId, uint256 received); event MinerHarvestFailed(uint256 indexed tokenId, bytes reason); event Harvested(address indexed caller, uint256 attempted, uint256 failed, uint256 received); event ReceiptIncomeClaimed( Category indexed category, uint256 indexed id, address indexed holder, uint256 amount ); event RecipientPaid(Recipient indexed role, address indexed recipient, uint256 amount); /// @notice C's part transferred to the sink by a release of `releasedRole` (A or B). event SinkPaid(Recipient indexed releasedRole, address indexed sink, uint256 amount); event RecipientChanged(Recipient indexed role, address indexed previous, address indexed current); constructor(Deployment memory d) Ownable(d.owner) { if (d.bem.code.length == 0 || d.mining.code.length == 0 || d.minerCollection.code.length == 0) { revert InvalidAddress(); } if (bytes(d.poolName).length == 0 || bytes(d.poolName).length > 64) revert InvalidName(); // Bind only to this exact receipt implementation (unique IDs, fixed supply after finalization). bytes32 receiptCode = keccak256(type(TapeoutEntitlementReceipt).runtimeCode); if ( d.nandReceipt == d.latchReceipt || d.nandReceipt.codehash != receiptCode || d.latchReceipt.codehash != receiptCode || TapeoutEntitlementReceipt(d.nandReceipt).category() != NAND_CATEGORY || TapeoutEntitlementReceipt(d.latchReceipt).category() != LATCH_CATEGORY ) revert IncompatibleReceipt(); if ( d.recipientA == address(0) || d.recipientB == address(0) || d.recipientA == address(this) || d.recipientB == address(this) || d.recipientA == SINK || d.recipientB == SINK ) revert InvalidAddress(); uint256 count = d.depositors.length; if (count == 0 || count > MAX_DEPOSITORS) revert InvalidDepositors(); for (uint256 i; i < count; ++i) { address a = d.depositors[i]; if ( a == address(0) || a == address(this) || isDepositor[a] || a == d.bem || a == d.mining || a == d.minerCollection || a == d.nandReceipt || a == d.latchReceipt ) revert InvalidDepositors(); isDepositor[a] = true; _depositors.push(a); } bem = IERC20(d.bem); mining = IMining(d.mining); minerCollection = IERC721(d.minerCollection); nandReceipt = TapeoutEntitlementReceipt(d.nandReceipt); latchReceipt = TapeoutEntitlementReceipt(d.latchReceipt); recipientA = d.recipientA; recipientB = d.recipientB; poolName = d.poolName; emit Deployed(d.nandReceipt, d.latchReceipt, d.recipientA, d.recipientB, d.depositors); } // --------------------------------------------------------------------------------------------- // Activation (owner, once) // --------------------------------------------------------------------------------------------- /// @notice Fix both category denominators. Both receipt collections must have finalized issuance, /// and the supplies must equal the values the operator reviewed. function activate(uint256 expectedNandShares, uint256 expectedLatchShares) external onlyOwner { if (active) revert AlreadyActive(); if (!nandReceipt.issuanceFinalized() || !latchReceipt.issuanceFinalized()) { revert IssuanceNotFinalized(); } uint256 sN = nandReceipt.totalSupply(); uint256 sL = latchReceipt.totalSupply(); if (sN != expectedNandShares || sL != expectedLatchShares) revert UnexpectedShareCounts(sN, sL); if (sN + sL == 0) revert NoShares(); nandShares = sN; latchShares = sL; active = true; activatedAt = uint64(block.timestamp); emit Activated(sN, sL, block.timestamp); } // --------------------------------------------------------------------------------------------- // Admission (whitelisted depositors, ACTIVE) // --------------------------------------------------------------------------------------------- /// @notice Admit miners owned by the caller (requires ERC-721 approval to this pool). function depositMiners(uint256[] calldata tokenIds) external nonReentrant returns (uint256 settledOldIncome, uint256 carriedIncome) { return _admit(tokenIds, true); } /// @notice Admit miners that already reached the pool by a plain transfer. Same whitelist and /// the same settlement boundary as `depositMiners`; a plain transfer alone admits nothing. function registerMiners(uint256[] calldata tokenIds) external nonReentrant returns (uint256 settledOldIncome, uint256 carriedIncome) { return _admit(tokenIds, false); } // --------------------------------------------------------------------------------------------- // Income collection (anyone, ACTIVE) // --------------------------------------------------------------------------------------------- /// @notice Recognize BEM that reached the pool by any route as income of the current period. function sync() external nonReentrant returns (uint256 received) { _requireActive(); received = _sync(); } /// @notice Ordinary harvest: claim the listed registered miners under the current composition. /// Failures are isolated per miner and reported; only actual balance increases count. function harvest(uint256[] calldata tokenIds) external nonReentrant returns (uint256 received, uint256 failed) { _requireActive(); if (tokenIds.length == 0) revert InvalidBatch(); _sync(); uint256 balance = bem.balanceOf(address(this)); for (uint256 i; i < tokenIds.length; ++i) { Miner storage m = _miners[tokenIds[i]]; if (!m.registered) revert UnknownMiner(tokenIds[i]); bool ok; (ok, balance) = _harvestOne(tokenIds[i], m.key, balance); if (!ok) ++failed; } received = _sync(); emit Harvested(msg.sender, tokenIds.length, failed, received); } /// @notice Harvest registry entries [start, start+count) (clamped to the registry length). function harvestRange(uint256 start, uint256 count) external nonReentrant returns (uint256 received, uint256 failed) { _requireActive(); uint256 length = _minerIds.length; if (count == 0 || start >= length) revert InvalidBatch(); uint256 end = count > length - start ? length : start + count; _sync(); uint256 balance = bem.balanceOf(address(this)); for (uint256 i = start; i < end; ++i) { bool ok; (ok, balance) = _harvestOne(_minerIds[i], _minerKeys[i], balance); if (!ok) ++failed; } received = _sync(); emit Harvested(msg.sender, end - start, failed, received); } // --------------------------------------------------------------------------------------------- // Claims and recipient payments (ACTIVE) // --------------------------------------------------------------------------------------------- /// @notice Claim the income attached to one receipt ID held by the caller. function claim(Category category, uint256 id) external nonReentrant returns (uint256 amount) { _requireActive(); _sync(); PoolMath.Accrual memory a = _accrual(totalAccounted); uint256 perShare = category == Category.NAND ? a.perNandShare : a.perLatchShare; amount = _claimOne(category, id, perShare, shareCount(category)); _receiptsPaid[uint256(category)] += amount; _pay(msg.sender, amount); } /// @notice Claim the income attached to any number of NAND and LATCH IDs held by the caller. function claimBatch(uint256[] calldata nandIds, uint256[] calldata latchIds) external nonReentrant returns (uint256 amount) { _requireActive(); if (nandIds.length == 0 && latchIds.length == 0) revert InvalidBatch(); _sync(); PoolMath.Accrual memory a = _accrual(totalAccounted); amount = _claimMany(Category.NAND, nandIds, a.perNandShare) + _claimMany(Category.LATCH, latchIds, a.perLatchShare); _pay(msg.sender, amount); } /// @notice Pay recipient A its unpaid 1/84 and, in the same transaction, C its whole unpaid 2/84 /// to `SINK`, both at one income snapshot. Callable by anyone; B is not affected. /// @return amount Paid to A (C's payment is reported by `SinkPaid`). function releaseA() external nonReentrant returns (uint256 amount) { return _release(Recipient.A); } /// @notice Pay recipient B its unpaid 1/84 and, in the same transaction, C its whole unpaid 2/84 /// to `SINK`, both at one income snapshot. Callable by anyone; A is not affected. /// @return amount Paid to B (C's payment is reported by `SinkPaid`). function releaseB() external nonReentrant returns (uint256 amount) { return _release(Recipient.B); } /// @notice A recipient may move its own role (e.g. to a new multisig). Unpaid income stays with /// the role. Nobody else, including the owner, can change a recipient. C is not a role and /// cannot be moved, and no role may be moved to C's sink address. function setRecipient(Recipient role, address newRecipient) external { address current = role == Recipient.A ? recipientA : recipientB; if (msg.sender != current) revert Unauthorized(); if (newRecipient == address(0) || newRecipient == address(this) || newRecipient == SINK) { revert InvalidAddress(); } if (role == Recipient.A) recipientA = newRecipient; else recipientB = newRecipient; emit RecipientChanged(role, current, newRecipient); } // --------------------------------------------------------------------------------------------- // Views // --------------------------------------------------------------------------------------------- function depositors() external view returns (address[] memory) { return _depositors; } function receiptOf(Category category) public view returns (TapeoutEntitlementReceipt) { return category == Category.NAND ? nandReceipt : latchReceipt; } function shareCount(Category category) public view returns (uint256) { return category == Category.NAND ? nandShares : latchShares; } function state() public view returns (PoolMath.State memory) { return PoolMath.State({ nandBase: nandBase, latchBase: latchBase, participantBase: participantBase, totalNandBurn: totalNandBurn, totalLatchBurn: totalLatchBurn, nandShares: nandShares, latchShares: latchShares }); } /// @notice Allocation of the income already recognized (totalAccounted). function accrual() external view returns (PoolMath.Accrual memory) { return _accrual(totalAccounted); } /// @notice Allocation after the next sync (adds BEM held but not yet recognized). Income still /// inside Mining is not included anywhere: it becomes income only when it is received. function previewAccrual() public view returns (PoolMath.Accrual memory) { return _accrual(totalAccounted + unrecognizedBalance()); } /// @notice BEM held beyond the ledger. Stays unrecognized while no miner has been admitted. function unrecognizedBalance() public view returns (uint256) { uint256 balance = bem.balanceOf(address(this)); uint256 expected = totalAccounted - totalPaid; if (balance < expected) revert BalanceDecreased(balance, expected); if (totalNandBurn + totalLatchBurn == 0) return 0; return balance - expected; } function claimed(Category category, uint256 id) external view returns (uint256) { return _claimed[category][id]; } function receiptsPaid(Category category) external view returns (uint256) { return _receiptsPaid[uint256(category)]; } /// @notice Income the current holder of `id` can claim now (including unrecognized balance). function claimable(Category category, uint256 id) public view returns (uint256) { if (id == 0 || id > shareCount(category)) revert ReceiptIdOutOfRange(category, id); PoolMath.Accrual memory a = previewAccrual(); uint256 perShare = category == Category.NAND ? a.perNandShare : a.perLatchShare; return perShare - _claimed[category][id]; } function claimableBatch(Category category, uint256[] calldata ids) external view returns (uint256[] memory amounts, uint256 total) { amounts = new uint256[](ids.length); if (ids.length == 0) return (amounts, 0); uint256 limit = shareCount(category); PoolMath.Accrual memory a = previewAccrual(); uint256 perShare = category == Category.NAND ? a.perNandShare : a.perLatchShare; mapping(uint256 => uint256) storage history = _claimed[category]; for (uint256 i; i < ids.length; ++i) { if (ids[i] == 0 || ids[i] > limit) revert ReceiptIdOutOfRange(category, ids[i]); amounts[i] = perShare - history[ids[i]]; total += amounts[i]; } } function recipientClaimable(Recipient role) external view returns (uint256) { uint256 accrued = previewAccrual().recipientA; // A and B accrue identically return accrued - (role == Recipient.A ? recipientAPaid : recipientBPaid); } /// @notice C's part the next release of A or B transfers to `SINK` (including unrecognized balance). function sinkClaimable() external view returns (uint256) { return previewAccrual().sink - sinkPaid; } /// @notice Full ledger snapshot; see docs/ACCOUNTING.md for the identities. function reconciliation() external view returns (Reconciliation memory r) { r.bemBalance = bem.balanceOf(address(this)); r.totalAccounted = totalAccounted; r.totalPaid = totalPaid; uint256 expected = totalAccounted - totalPaid; if (r.bemBalance >= expected) r.unrecognized = r.bemBalance - expected; else r.deficit = expected - r.bemBalance; PoolMath.Accrual memory a = _accrual(totalAccounted); r.recipientAAccrued = a.recipientA; r.recipientAPaid = recipientAPaid; r.recipientBAccrued = a.recipientB; r.recipientBPaid = recipientBPaid; r.sinkAccrued = a.sink; r.sinkPaid = sinkPaid; r.nandIncome = a.nandIncome; r.nandReceiptsAccrued = a.nandReceipts; r.nandReceiptsPaid = _receiptsPaid[0]; r.latchIncome = a.latchIncome; r.latchReceiptsAccrued = a.latchReceipts; r.latchReceiptsPaid = _receiptsPaid[1]; r.residue = a.residue; } function minerCount() external view returns (uint256) { return _minerIds.length; } function miner(uint256 tokenId) external view returns (Miner memory) { return _miners[tokenId]; } function minerAt(uint256 index) external view returns (uint256 tokenId, bytes32 key) { return (_minerIds[index], _minerKeys[index]); } function minersPage(uint256 offset, uint256 limit) external view returns (uint256[] memory tokenIds, bytes32[] memory keys) { uint256 length = _minerIds.length; if (limit == 0 || limit > MAX_PAGE || offset > length) revert InvalidPage(); uint256 count = limit > length - offset ? length - offset : limit; tokenIds = new uint256[](count); keys = new bytes32[](count); for (uint256 i; i < count; ++i) { tokenIds[i] = _minerIds[offset + i]; keys[i] = _minerKeys[offset + i]; } } // --------------------------------------------------------------------------------------------- // Internals // --------------------------------------------------------------------------------------------- function _requireActive() private view { if (!active) revert NotActive(); } function _admit(uint256[] calldata tokenIds, bool pull) private returns (uint256 settled, uint256 carried) { _requireActive(); if (!isDepositor[msg.sender]) revert NotDepositor(msg.sender); uint256 n = tokenIds.length; if (n == 0 || n > MAX_ADMISSION_BATCH) revert InvalidBatch(); // 1. Validate the batch and read each miner's authoritative burn from Mining. bytes32[] memory keys = new bytes32[](n); uint32[] memory nands = new uint32[](n); uint32[] memory latches = new uint32[](n); uint256 addNand; uint256 addLatch; for (uint256 i; i < n; ++i) { uint256 id = tokenIds[i]; for (uint256 j; j < i; ++j) { if (tokenIds[j] == id) revert DuplicateMiner(id); } if (_miners[id].registered) revert MinerAlreadyRegistered(id); address holder = minerCollection.ownerOf(id); if (holder != (pull ? msg.sender : address(this))) revert MinerNotHeld(id, holder); (keys[i], nands[i], latches[i]) = _readComposition(id); addNand += nands[i]; addLatch += latches[i]; } // A category without receipts cannot receive income; refuse instead of redirecting it. if (addNand != 0 && nandShares == 0) revert UnsupportedCategory(Category.NAND); if (addLatch != 0 && latchShares == 0) revert UnsupportedCategory(Category.LATCH); // 2. BEM already held belongs to the old composition. _sync(); // 3. Collect everything the registered miners can pay now, under the old composition. uint256 oldCount = _minerKeys.length; for (uint256 i; i < oldCount; ++i) { _settleOld(i); } settled = _sync(); // 4. Close the old period, take custody and switch to the post-batch composition. _checkpoint(); uint256 period = compositionPeriod; for (uint256 i; i < n; ++i) { uint256 id = tokenIds[i]; if (pull) minerCollection.transferFrom(msg.sender, address(this), id); if (minerCollection.ownerOf(id) != address(this)) { revert MinerNotHeld(id, minerCollection.ownerOf(id)); } _miners[id] = Miner({ key: keys[i], nandBurn: nands[i], latchBurn: latches[i], admittedInPeriod: uint64(period), registered: true }); _minerIds.push(id); _minerKeys.push(keys[i]); emit MinerRegistered(id, keys[i], msg.sender, nands[i], latches[i]); } totalNandBurn += addNand; totalLatchBurn += addLatch; // 5. Income the incoming miners carried in belongs to the pool, under the new composition. for (uint256 i; i < n; ++i) { mining.claim(keys[i]); } carried = _sync(); emit MinersAdmitted(msg.sender, period, n, settled, carried, totalNandBurn, totalLatchBurn); } /// @dev Reads the miner record and cross-checks it with the burn cache: both must agree, the miner /// must be ACTIVE and belong to this collection/token, and it must have burned something. function _readComposition(uint256 tokenId) private view returns (bytes32 key, uint32 nand, uint32 latch) { key = mining.minerKey(address(minerCollection), tokenId); IMining.MinerRecord memory r = mining.getMiner(key); if ( r.status != MINER_ACTIVE || r.circuits != address(minerCollection) || uint256(r.circuitId) != tokenId ) { revert MinerNotActive(tokenId); } (bool cached, uint32 cachedNand, uint32 cachedLatch) = mining.cachedBurn(address(minerCollection), tokenId); if (!cached || cachedNand != r.nandBurn || cachedLatch != r.latchBurn) { revert CompositionMismatch(tokenId); } if (uint256(r.nandBurn) + r.latchBurn == 0) revert EmptyComposition(tokenId); return (key, r.nandBurn, r.latchBurn); } /// @dev Strict settlement of one registered miner: any failed claim aborts the admission, with one /// exception. A miner Mining reports as REVOKED is permanent and Mining refuses every claim for /// it, so nothing of it can ever be collected; it is skipped with an event. A pool-held miner /// cannot be stopped (only the NFT holder can call Mining.stop, and the pool never does), so a /// COOLING or never-started status here is unexpected and fails closed like any other failure. function _settleOld(uint256 index) private { bytes32 key = _minerKeys[index]; try mining.claim(key) {} catch (bytes memory reason) { if (mining.getMiner(key).status != MINER_REVOKED) { revert SettlementFailed(_minerIds[index], reason); } emit RevokedMinerSkipped(_minerIds[index]); } } function _checkpoint() private { PoolMath.Accrual memory a = _accrual(totalAccounted); nandBase = a.nandIncome; latchBase = a.latchIncome; participantBase = a.nandIncome + a.latchIncome; ++compositionPeriod; emit CompositionCheckpoint(compositionPeriod, totalAccounted, nandBase, latchBase, participantBase); } function _harvestOne(uint256 tokenId, bytes32 key, uint256 beforeBalance) private returns (bool ok, uint256 balance) { try mining.claim(key) { balance = bem.balanceOf(address(this)); emit MinerHarvested(tokenId, balance > beforeBalance ? balance - beforeBalance : 0); ok = true; } catch (bytes memory reason) { balance = beforeBalance; emit MinerHarvestFailed(tokenId, reason); } } function _claimMany(Category category, uint256[] calldata ids, uint256 perShare) private returns (uint256 total) { if (ids.length == 0) return 0; uint256 limit = shareCount(category); for (uint256 i; i < ids.length; ++i) { total += _claimOne(category, ids[i], perShare, limit); } _receiptsPaid[uint256(category)] += total; } function _claimOne(Category category, uint256 id, uint256 perShare, uint256 limit) private returns (uint256 due) { if (id == 0 || id > limit) revert ReceiptIdOutOfRange(category, id); // Only the current holder can claim; ERC-1155 operators must transfer the ID first. if (receiptOf(category).balanceOf(msg.sender, id) != 1) revert NotHolder(category, id); mapping(uint256 => uint256) storage history = _claimed[category]; due = perShare - history[id]; history[id] = perShare; emit ReceiptIncomeClaimed(category, id, msg.sender, due); } /// @dev One snapshot of the recognized total for both payments. The selected role receives its own /// unpaid part; C receives all of its unpaid part (`sink - sinkPaid`), never a multiple of this /// call's role payment, since A and B release at different times and round independently. function _release(Recipient role) private returns (uint256 amount) { _requireActive(); _sync(); PoolMath.Accrual memory a = _accrual(totalAccounted); address to; if (role == Recipient.A) { amount = a.recipientA - recipientAPaid; recipientAPaid = a.recipientA; to = recipientA; } else { amount = a.recipientB - recipientBPaid; recipientBPaid = a.recipientB; to = recipientB; } uint256 sinkAmount = a.sink - sinkPaid; sinkPaid = a.sink; _payWithSink(to, amount, sinkAmount); emit RecipientPaid(role, to, amount); emit SinkPaid(role, SINK, sinkAmount); } function _accrual(uint256 total) private view returns (PoolMath.Accrual memory) { return PoolMath.accrue(total, state()); } /// @dev Recognize BEM that arrived since the last sync. Nothing is recognized while no miner has /// been admitted (there is no composition to split it by); it is recognized at the first admission. function _sync() private returns (uint256 received) { if (totalNandBurn + totalLatchBurn == 0) return 0; uint256 balance = bem.balanceOf(address(this)); uint256 expected = totalAccounted - totalPaid; if (balance < expected) revert BalanceDecreased(balance, expected); received = balance - expected; if (received != 0) { totalAccounted += received; emit IncomeRecognized(received, totalAccounted, compositionPeriod); } } /// @dev A release's two payments as one unit: every counter (the role's and C's paid amounts, set by /// the caller, and `totalPaid` here) changes before either transfer, and both transfers and all /// three balance deltas must be exact, or the whole release reverts. `to` is never `SINK` or this /// pool (both refused for A and B). function _payWithSink(address to, uint256 amount, uint256 sinkAmount) private { uint256 outflow = amount + sinkAmount; if (outflow == 0) return; uint256 selfBefore = bem.balanceOf(address(this)); uint256 toBefore = bem.balanceOf(to); uint256 sinkBefore = bem.balanceOf(SINK); totalPaid += outflow; if (amount != 0) bem.safeTransfer(to, amount); if (sinkAmount != 0) bem.safeTransfer(SINK, sinkAmount); if ( bem.balanceOf(address(this)) != selfBefore - outflow || bem.balanceOf(to) != toBefore + amount || bem.balanceOf(SINK) != sinkBefore + sinkAmount ) revert UnexpectedTokenBehavior(); } /// @dev Plain, non-rebasing, fee-free BEM only: both balance deltas must be exact. function _pay(address to, uint256 amount) private { if (amount == 0) return; uint256 selfBefore = bem.balanceOf(address(this)); uint256 toBefore = bem.balanceOf(to); totalPaid += amount; bem.safeTransfer(to, amount); if (bem.balanceOf(address(this)) != selfBefore - amount || bem.balanceOf(to) != toBefore + amount) { revert UnexpectedTokenBehavior(); } } }