// SPDX-License-Identifier: UNLICENSED // Proprietary - (c) 2026 Durian (durianfun). All rights reserved. Not open source; for Durianfun + authorized integrations only. See /LICENSE. pragma solidity 0.8.24; import "./IEntropyConsumer.sol"; import "./DurianEntropyV21.sol"; /// @title EntropyConsumerV21 /// @notice Inherit this and implement ONE function (`_onRandomness`) to wire your app to /// DurianEntropyV21. It handles the protocol fee + bounty, callback PROVENANCE (anti-spoof), /// double-delivery protection, a `settle()` recovery path, self-crank helpers, and reclaiming /// any keeper bounty this consumer earns by self-cranking. /// /// @dev DO NOT implement IEntropyConsumer raw. Because anyone can call /// `entropy.requestCallback(seed, , cbGas)` and point a request at YOUR contract, /// a raw `onRandomness` that trusts every callback can be spoofed: it would run your /// payout/mint logic for an id you never created. This base records `known[id]` in `_roll()` /// and requires it in `_deliver()`, so a foreign-originated callback is rejected. abstract contract EntropyConsumerV21 is IEntropyConsumer { DurianEntropyV21 public immutable entropy; /// @dev Guards against the same id being delivered twice (callback AND settle()). mapping(uint256 => bool) public delivered; /// @dev Callback PROVENANCE: ids this consumer actually created via _roll(). _deliver requires it, /// so a spoofed callback for a foreign id (someone else's requestCallback pointed here) reverts. mapping(uint256 => bool) public known; constructor(address e) { entropy = DurianEntropyV21(e); } /// @notice Request randomness, paying the current fee (bounty + protocol). The user signs only the /// tx that calls this; settlement is done later by a keeper, the next user (self-crank), or /// the requester. The escrowed bounty reimburses whoever reveals. /// @dev Your contract must hold `entropy.fee(cbGas)` (collect it from the user, refund overpay). function _roll(bytes32 seed, uint32 cbGas) internal returns (uint256 id) { id = entropy.requestCallback{value: entropy.fee(cbGas)}(seed, address(this), cbGas); known[id] = true; // mark as ours so a spoofed callback for a foreign id is rejected } /// @notice Self-crank helper: settle a previously-pending request if ready, ignoring failure (not /// ready / already settled / expired). Fires that request's callback AND credits this /// consumer the bounty (it is the revealer) — reclaim it with claimEntropyBounty(). function _tryReveal(uint256 id) internal { try entropy.reveal(id) {} catch {} } /// @notice Cheap capture helper: lock the blockhash within the 256-block window (no bounty; the /// bounty pays the REVEALER). Only needed if you must defer the reveal past the window. function _trySeal(uint256 id) internal { try entropy.seal(id) {} catch {} } /// @notice Reclaim bounty this consumer earned by self-cranking (entropy credits the REVEALER, which /// is this contract when it calls _tryReveal). Permissionless poke; funds land in this /// contract (handle/forward them in your own logic). Without this, self-crank bounty is stuck. function claimEntropyBounty() external { entropy.withdraw(); } /// @dev Accept the bounty payout from entropy.withdraw(). Override in a money consumer if you must /// gate incoming ETH, but keep a path for the withdraw to succeed. receive() external payable {} /// @inheritdoc IEntropyConsumer /// @dev Only DurianEntropyV21 may call this. Reverting here is safe: entropy catches it (the bounty /// still pays the revealer) and you can recover delivery via settle(). function onRandomness(uint256 id, uint256 rng) external override { require(msg.sender == address(entropy), "only entropy"); _deliver(id, rng); } /// @notice Recovery: if the on-reveal callback failed, anyone can poke this once the request is /// FULFILLED to deliver the stored result exactly once. function settle(uint256 id) external { require(entropy.status(id) == DurianEntropyV21.Status.FULFILLED, "not fulfilled"); _deliver(id, entropy.results(id)); } function _deliver(uint256 id, uint256 rng) private { require(known[id], "unknown request"); // only ids WE created via _roll (anti-spoof) require(!delivered[id], "already delivered"); delivered[id] = true; _onRandomness(id, rng); } /// @dev Implement your settlement logic here. Called exactly once per id. function _onRandomness(uint256 id, uint256 rng) internal virtual; }