{"language":"Solidity","sources":{"src/v2/Farm.sol":{"content":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Ownable} from \"openzeppelin-contracts/contracts/access/Ownable.sol\";\n\ninterface IFarmAnimal {\n    function ownerOf(uint256 id) external view returns (address);\n    function roleOf(uint256 id) external view returns (uint8);\n    function isGenesis(uint256 id) external view returns (bool);\n    function revealed(uint256 id) external view returns (bool);\n    function bornAt(uint256 id) external view returns (uint64);\n    function alpha(uint256 id) external view returns (uint8);\n}\n\ninterface IWool {\n    function transfer(address to, uint256 amount) external returns (bool);\n    function transferFrom(address from, address to, uint256 amount) external returns (bool);\n    function balanceOf(address who) external view returns (uint256);\n}\n\n/// @title Farm v4 — 风险在收割时结算：抽水 + 概率掉落，收益进狼池\n/// @notice 规则（对标 BNBCAT，但动物永远不会被销毁）：\n///         1) 每次收割必扣 20% 进狼池（有守卫 12%）\n///         2) 另有 1/5 概率整批掉落：这一批全进狼池，玩家这一批拿 0，羊仍在\n///         3) 狼池由 keeper 每周按持狼数量分给狼主\n///         4) 产出最多累积 7 天；喂食 +250 单独累加\ncontract Farm is Ownable {\n    IFarmAnimal public immutable animals;\n    IWool public wool;\n\n    uint256 public constant DAY = 1 days;\n    uint256 public constant MAX_ACCRUAL_DAYS = 7;\n    uint256 public constant SHEEP_PER_DAY = 1250e18;\n    uint256 public constant FEED_BONUS = 250e18;\n\n    uint256 public constant TOLL_BPS = 2_000;\n    uint256 public constant TOLL_BPS_GUARDED = 1_200;\n    uint256 public constant DROP_CHANCE = 20;\n    uint256 public constant GUARD_PRICE = 500e18;\n    uint256 public constant GUARD_DAYS = 7 days;\n\n    address public constant TREASURY = 0x29F64d560bB694963750F7F1822155DBaF440388;\n\n    uint256 public reserve;          // 还没有狼时，钱先躺在这里\n    uint256 public accPerWeight;     // 每 1 份权重的累计收益（1e18 精度）\n    uint256 public totalWeight;      // 已登记的狼的总权重\n\n    mapping(uint256 => bool) public registered;\n    mapping(uint256 => uint256) public paidWolf;\n\n    mapping(uint256 => uint256) public lastHarvest;\n    mapping(uint256 => uint256) public bonus;\n    mapping(uint256 => uint256) public fedOnDay;\n    mapping(address => uint256) public feedTickDay;\n    mapping(address => uint256) public feed;\n    mapping(address => uint256) public guardedUntil;\n\n    event WoolSet(address indexed wool);\n    event Harvested(address indexed who, uint256 count, uint256 paid, uint256 tolled, uint256 dropped);\n    event Fed(address indexed who, uint256 indexed id, uint256 bonus);\n    event GuardBought(address indexed who, uint256 until, uint256 paid);\n    event WolfClaimed(address indexed who, uint256 indexed id, uint256 amount);\n    event WolfRegistered(uint256 indexed id, uint256 weight, uint256 totalWeight);\n\n    error NotOwner();\n    error BadRole();\n    error AlreadyDone();\n    error NoFeed();\n    error NoWool();\n    error EmptyRewardPool();\n    error TooMuch();\n    error BadLen();\n\n    constructor(IFarmAnimal animals_, address initialOwner) Ownable(initialOwner) {\n        animals = animals_;\n    }\n\n    function setWool(address wool_) external onlyOwner {\n        if (address(wool) != address(0)) revert AlreadyDone();\n        wool = IWool(wool_);\n        emit WoolSet(wool_);\n    }\n\n    // ── 产出 ────────────────────────────────────────────────────────────\n\n    function dayOf(uint256 ts) public pure returns (uint256) { return ts / DAY; }\n\n    function pending(uint256 id) public view returns (uint256) {\n        if (!animals.revealed(id)) return 0;\n        if (animals.roleOf(id) != 0) return 0;\n        uint256 since = lastHarvest[id] != 0 ? lastHarvest[id] : uint256(animals.bornAt(id));\n        if (since == 0 || since > block.timestamp) return 0;\n        uint256 elapsed = block.timestamp - since;\n        if (elapsed > MAX_ACCRUAL_DAYS * DAY) elapsed = MAX_ACCRUAL_DAYS * DAY;\n        uint256 gain = (elapsed * SHEEP_PER_DAY) / DAY;\n        if (animals.isGenesis(id)) gain = (gain * 120) / 100;\n        return gain;\n    }\n\n    function pendingTotal(uint256 id) public view returns (uint256) {\n        return pending(id) + bonus[id];\n    }\n\n    /// @notice 结果只跟上一个区块的哈希、id 和调用者有关，事后可复算\n    function wouldDrop(uint256 id, address who) public view returns (bool) {\n        return uint256(keccak256(abi.encodePacked(blockhash(block.number - 1), id, who))) % 100 < DROP_CHANCE;\n    }\n\n    /// @notice 收割：抽水 20%（守卫 12%），另 1/5 概率整批掉落\n    function harvest(uint256[] calldata ids) external {\n        if (address(wool) == address(0)) revert NoWool();\n\n        uint256 paid;\n        uint256 tolled;\n        uint256 dropped;\n        uint256 bps = guardedUntil[msg.sender] > block.timestamp ? TOLL_BPS_GUARDED : TOLL_BPS;\n\n        for (uint256 i = 0; i < ids.length; i++) {\n            uint256 id = ids[i];\n            if (animals.ownerOf(id) != msg.sender) revert NotOwner();\n            uint256 amount = pending(id) + bonus[id];\n            bonus[id] = 0;\n            lastHarvest[id] = block.timestamp;\n            if (amount == 0) continue;\n\n            bool drop = uint256(keccak256(abi.encodePacked(blockhash(block.number - 1), id, msg.sender))) % 100 < DROP_CHANCE;\n            if (drop) {\n                dropped += amount;\n            } else {\n                uint256 toll = (amount * bps) / 10_000;\n                tolled += toll;\n                paid += amount - toll;\n            }\n        }\n\n        uint256 out = paid + dropped + tolled;\n        if (out == 0) return;\n        if (wool.balanceOf(address(this)) < out) revert EmptyRewardPool();\n\n        _accrue(dropped + tolled);\n        if (paid > 0) require(wool.transfer(msg.sender, paid), \"transfer\");\n\n        emit Harvested(msg.sender, ids.length, paid, tolled, dropped);\n    }\n\n    function feedTick(address who, uint256 amount) external onlyOwner {\n        uint256 d = dayOf(block.timestamp) + 1;\n        if (feedTickDay[who] == d) return;\n        feedTickDay[who] = d;\n        feed[who] += amount;\n    }\n\n    function feedSheep(uint256 id, uint256 times) external {\n        if (animals.ownerOf(id) != msg.sender) revert NotOwner();\n        if (animals.roleOf(id) != 0) revert BadRole();\n        uint256 d = dayOf(block.timestamp) + 1;\n        if (fedOnDay[id] == d) revert AlreadyDone();\n        if (feed[msg.sender] < times || times == 0) revert NoFeed();\n        feed[msg.sender] -= times;\n        fedOnDay[id] = d;\n        bonus[id] += FEED_BONUS;\n        emit Fed(msg.sender, id, FEED_BONUS);\n    }\n\n    // ── 守卫 ────────────────────────────────────────────────────────────\n\n    function buyGuard() external {\n        if (address(wool) == address(0)) revert NoWool();\n        uint256 base = guardedUntil[msg.sender] > block.timestamp ? guardedUntil[msg.sender] : block.timestamp;\n        require(wool.transferFrom(msg.sender, TREASURY, GUARD_PRICE), \"transferFrom\");\n        guardedUntil[msg.sender] = base + GUARD_DAYS;\n        emit GuardBought(msg.sender, guardedUntil[msg.sender], GUARD_PRICE);\n    }\n\n    function isGuarded(address who) external view returns (bool) {\n        return guardedUntil[who] > block.timestamp;\n    }\n\n    function setGuard(address who, bool on) external onlyOwner {\n        guardedUntil[who] = on ? block.timestamp + GUARD_DAYS : 0;\n    }\n\n    // ── 狼池分配（keeper 每周调用）──────────────────────────────────────\n\n    // ── 狼池：随时可领 ──────────────────────────────────────────────────\n\n    function weightOf(uint256 id) public view returns (uint256) {\n        if (animals.roleOf(id) != 1 || !animals.revealed(id)) return 0;\n        uint256 alpha = animals.alpha(id);\n        if (alpha == 0) alpha = 6;\n        uint256 base = animals.isGenesis(id) ? 120 : 100;\n        return base * alpha;\n    }\n\n    /// @notice 这只狼现在能领多少\n    function claimable(uint256 id) public view returns (uint256) {\n        uint256 w = weightOf(id);\n        if (w == 0) return 0;\n        uint256 earned = (w * accPerWeight) / 1e18;\n        uint256 paid = paidWolf[id];\n        return earned > paid ? earned - paid : 0;\n    }\n\n    /// @notice 新增的狼登记一次：之后才参与分账，不会分到它出生前的池子边角\n    function syncWolves(uint256[] calldata ids) external onlyOwner {\n        uint256 added;\n        for (uint256 i = 0; i < ids.length; i++) {\n            uint256 id = ids[i];\n            if (registered[id]) continue;\n            uint256 w = weightOf(id);\n            if (w == 0) continue;\n            registered[id] = true;\n            paidWolf[id] = (w * accPerWeight) / 1e18;\n            totalWeight += w;\n            added += w;\n            emit WolfRegistered(id, w, totalWeight);\n        }\n        if (added > 0 && reserve > 0 && totalWeight > 0) {\n            accPerWeight += (reserve * 1e18) / totalWeight;\n            reserve = 0;\n        }\n    }\n\n    /// @notice 狼主随时来领，先到先得；池子空了就等下一笔\n    function claimWolf(uint256 id) external returns (uint256 amount) {\n        if (animals.ownerOf(id) != msg.sender) revert NotOwner();\n        if (!registered[id]) revert BadRole();\n        amount = claimable(id);\n        if (amount == 0) return 0;\n        paidWolf[id] = (weightOf(id) * accPerWeight) / 1e18;\n        require(wool.transfer(msg.sender, amount), \"transfer\");\n        emit WolfClaimed(msg.sender, id, amount);\n    }\n\n    function _accrue(uint256 amount) internal {\n        if (amount == 0) return;\n        if (totalWeight == 0) {\n            reserve += amount;\n        } else {\n            accPerWeight += (amount * 1e18) / totalWeight;\n        }\n    }\n}\n"},"lib/openzeppelin-contracts/contracts/access/Ownable.sol":{"content":"// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.0.0) (access/Ownable.sol)\n\npragma solidity ^0.8.20;\n\nimport {Context} from \"../utils/Context.sol\";\n\n/**\n * @dev Contract module which provides a basic access control mechanism, where\n * there is an account (an owner) that can be granted exclusive access to\n * specific functions.\n *\n * The initial owner is set to the address provided by the deployer. This can\n * later be changed with {transferOwnership}.\n *\n * This module is used through inheritance. It will make available the modifier\n * `onlyOwner`, which can be applied to your functions to restrict their use to\n * the owner.\n */\nabstract contract Ownable is Context {\n    address private _owner;\n\n    /**\n     * @dev The caller account is not authorized to perform an operation.\n     */\n    error OwnableUnauthorizedAccount(address account);\n\n    /**\n     * @dev The owner is not a valid owner account. (eg. `address(0)`)\n     */\n    error OwnableInvalidOwner(address owner);\n\n    event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);\n\n    /**\n     * @dev Initializes the contract setting the address provided by the deployer as the initial owner.\n     */\n    constructor(address initialOwner) {\n        if (initialOwner == address(0)) {\n            revert OwnableInvalidOwner(address(0));\n        }\n        _transferOwnership(initialOwner);\n    }\n\n    /**\n     * @dev Throws if called by any account other than the owner.\n     */\n    modifier onlyOwner() {\n        _checkOwner();\n        _;\n    }\n\n    /**\n     * @dev Returns the address of the current owner.\n     */\n    function owner() public view virtual returns (address) {\n        return _owner;\n    }\n\n    /**\n     * @dev Throws if the sender is not the owner.\n     */\n    function _checkOwner() internal view virtual {\n        if (owner() != _msgSender()) {\n            revert OwnableUnauthorizedAccount(_msgSender());\n        }\n    }\n\n    /**\n     * @dev Leaves the contract without owner. It will not be possible to call\n     * `onlyOwner` functions. Can only be called by the current owner.\n     *\n     * NOTE: Renouncing ownership will leave the contract without an owner,\n     * thereby disabling any functionality that is only available to the owner.\n     */\n    function renounceOwnership() public virtual onlyOwner {\n        _transferOwnership(address(0));\n    }\n\n    /**\n     * @dev Transfers ownership of the contract to a new account (`newOwner`).\n     * Can only be called by the current owner.\n     */\n    function transferOwnership(address newOwner) public virtual onlyOwner {\n        if (newOwner == address(0)) {\n            revert OwnableInvalidOwner(address(0));\n        }\n        _transferOwnership(newOwner);\n    }\n\n    /**\n     * @dev Transfers ownership of the contract to a new account (`newOwner`).\n     * Internal function without access restriction.\n     */\n    function _transferOwnership(address newOwner) internal virtual {\n        address oldOwner = _owner;\n        _owner = newOwner;\n        emit OwnershipTransferred(oldOwner, newOwner);\n    }\n}\n"},"lib/openzeppelin-contracts/contracts/utils/Context.sol":{"content":"// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.0.1) (utils/Context.sol)\n\npragma solidity ^0.8.20;\n\n/**\n * @dev Provides information about the current execution context, including the\n * sender of the transaction and its data. While these are generally available\n * via msg.sender and msg.data, they should not be accessed in such a direct\n * manner, since when dealing with meta-transactions the account sending and\n * paying for execution may not be the actual sender (as far as an application\n * is concerned).\n *\n * This contract is only required for intermediate, library-like contracts.\n */\nabstract contract Context {\n    function _msgSender() internal view virtual returns (address) {\n        return msg.sender;\n    }\n\n    function _msgData() internal view virtual returns (bytes calldata) {\n        return msg.data;\n    }\n\n    function _contextSuffixLength() internal view virtual returns (uint256) {\n        return 0;\n    }\n}\n"}},"settings":{"remappings":["lib/openzeppelin-contracts/:forge-std/=lib/openzeppelin-contracts/lib/forge-std/src/","lib/openzeppelin-contracts/:forge-std/=lib/openzeppelin-contracts/lib/forge-std/src/","@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/","erc4626-tests/=lib/openzeppelin-contracts/lib/erc4626-tests/","forge-std/=lib/forge-std/src/","halmos-cheatcodes/=lib/openzeppelin-contracts/lib/halmos-cheatcodes/src/","openzeppelin-contracts/=lib/openzeppelin-contracts/"],"optimizer":{"enabled":true,"runs":200},"metadata":{"useLiteralContent":false,"bytecodeHash":"ipfs","appendCBOR":true},"outputSelection":{"*":{"*":["abi","evm.bytecode.object","evm.bytecode.sourceMap","evm.bytecode.linkReferences","evm.deployedBytecode.object","evm.deployedBytecode.sourceMap","evm.deployedBytecode.linkReferences","evm.deployedBytecode.immutableReferences","evm.methodIdentifiers","metadata"]}},"evmVersion":"cancun","viaIR":true,"libraries":{}}}
