// SPDX-License-Identifier: MIT pragma solidity 0.8.26; import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; interface IPlantRounds { function plantFor(address who, uint32 mask, uint256 each) external payable; function currentRound() external view returns (uint256); } /// @title Gardener vault — let a keeper plant for you every round /// @notice Deposit HYPE and pick a strategy. The keeper can only plant from your balance, only on the rounds /// contract, only in your name, and only within your rules. Winnings are paid straight to your wallet. /// Withdraw any time. 1% of what the gardener plants goes to the team. No owner. contract AmbrosiaGardener is ReentrancyGuard { enum Mode { Off, Fixed, // exactly the plots in `fixedMask` Count // any `count` plots, chosen by the keeper (random or quietest) } struct Strategy { Mode mode; uint32 fixedMask; uint8 count; uint96 each; // HYPE per plot uint96 floor; // stop when the balance would fall below this uint32 maxRounds; // 0 = no limit uint32 played; } uint256 public constant FEE_BPS = 100; IPlantRounds public immutable rounds; address public immutable keeperA; address public immutable keeperB; address public immutable team; mapping(address => uint256) public balanceOf; mapping(address => Strategy) public strategyOf; mapping(address => uint256) public lastRound; uint256 public teamHype; event Deposited(address indexed who, uint256 amount); event Withdrawn(address indexed who, uint256 amount); event StrategySet(address indexed who, Strategy s); event Played(address indexed who, uint256 indexed round, uint32 mask, uint256 spent, uint256 fee); event Skipped(address indexed who, uint256 indexed round, uint8 reason); error NotKeeper(); error BadStrategy(); error LengthMismatch(); error SendFailed(); constructor(IPlantRounds rounds_, address keeperA_, address keeperB_, address team_) { rounds = rounds_; keeperA = keeperA_; keeperB = keeperB_; team = team_; } function deposit() external payable { balanceOf[msg.sender] += msg.value; emit Deposited(msg.sender, msg.value); } function withdraw(uint256 amount) external nonReentrant { if (amount > balanceOf[msg.sender]) amount = balanceOf[msg.sender]; balanceOf[msg.sender] -= amount; (bool ok,) = msg.sender.call{value: amount}(""); if (!ok) revert SendFailed(); emit Withdrawn(msg.sender, amount); } function setStrategy(Mode mode, uint32 fixedMask, uint8 count, uint96 each, uint96 floor, uint32 maxRounds) external { if (mode == Mode.Fixed && (fixedMask == 0 || fixedMask >= (1 << 25))) revert BadStrategy(); if (mode == Mode.Count && (count == 0 || count > 25)) revert BadStrategy(); if (mode != Mode.Off && each < 0.01 ether) revert BadStrategy(); Strategy memory s = Strategy(mode, fixedMask, count, each, floor, maxRounds, 0); strategyOf[msg.sender] = s; emit StrategySet(msg.sender, s); } /// @notice Keeper plants for each user in the current round. Users who break a rule are skipped, not reverted. function play(address[] calldata who, uint32[] calldata masks) external nonReentrant { if (msg.sender != keeperA && msg.sender != keeperB) revert NotKeeper(); if (who.length != masks.length) revert LengthMismatch(); uint256 round = rounds.currentRound(); for (uint256 i; i < who.length; ++i) { address u = who[i]; Strategy storage s = strategyOf[u]; uint32 mask = masks[i]; if (s.mode == Mode.Off) { emit Skipped(u, round, 1); continue; } if (lastRound[u] == round + 1) { emit Skipped(u, round, 2); // already played this round continue; } if (s.maxRounds != 0 && s.played >= s.maxRounds) { emit Skipped(u, round, 3); continue; } uint256 n = _popcount(mask); bool ok = s.mode == Mode.Fixed ? mask == s.fixedMask : (n == s.count && mask < (1 << 25)); if (!ok) { emit Skipped(u, round, 4); continue; } uint256 spend = n * s.each; uint256 fee = spend * FEE_BPS / 10_000; uint256 bal = balanceOf[u]; if (bal < spend + fee || bal - spend - fee < s.floor) { emit Skipped(u, round, 5); continue; } try rounds.plantFor{value: spend}(u, mask, s.each) { balanceOf[u] = bal - spend - fee; teamHype += fee; lastRound[u] = round + 1; s.played += 1; emit Played(u, round, mask, spend, fee); } catch { emit Skipped(u, round, 6); } } } function withdrawTeam() external nonReentrant { uint256 a = teamHype; teamHype = 0; (bool ok,) = team.call{value: a}(""); if (!ok) revert SendFailed(); } function _popcount(uint32 x) private pure returns (uint256 c) { while (x != 0) { x &= x - 1; ++c; } } }