// SPDX-License-Identifier: MIT pragma solidity 0.8.26; interface INectarSeed { function balanceOf(address a) external view returns (uint256); function levelOfHolder(address a) external view returns (uint8); function shares() external view returns (address); } interface INectarShares { function sharesOf(address a) external view returns (uint256); } /// @title Ambrosia Nectar pool /// @notice HYPE sent here is shared, one UTC day at a time, between wallets that hold a Seed and take part. /// points = paid shares the wallet owns (at most 50 count), doubled if its Seed is a Bloom /// Each UTC day is its own pot: HYPE that arrives during a day is split between the wallets checked in /// that day, by points, and can be claimed from 00:00 UTC the next day, with no deadline. /// Anyone can check any wallet in (it only reads live state), so the keeper does it for everyone; a wallet /// can also do it itself. A day with HYPE but nobody checked in rolls into the current day. /// No owner. Nothing here can be paused, changed or taken out except by the wallets it belongs to. contract AmbrosiaNectar { uint256 public constant MAX_SHARES_COUNTED = 50; INectarSeed public immutable seed; mapping(uint256 day => uint256) public pot; mapping(uint256 day => uint256) public totalPoints; mapping(uint256 day => mapping(address => uint256)) public pointsOf; mapping(uint256 day => mapping(address => bool)) public claimed; uint256 public totalFunded; uint256 public totalClaimed; uint256 private _lock = 1; event Funded(uint256 indexed day, address indexed from, uint256 amount); event CheckedIn(uint256 indexed day, address indexed who, uint256 points); event Claimed(address indexed who, uint256 amount); event Rolled(uint256 indexed fromDay, uint256 indexed toDay, uint256 amount); error Reentrancy(); error TransferFailed(); constructor(INectarSeed seed_) { seed = seed_; } function today() public view returns (uint256) { return block.timestamp / 1 days; } receive() external payable { _fund(); } function fund() external payable { _fund(); } function _fund() private { if (msg.value == 0) return; uint256 d = today(); pot[d] += msg.value; totalFunded += msg.value; emit Funded(d, msg.sender, msg.value); } /// @notice Points a wallet would get if checked in now. 0 without a Seed or without a paid share. function pointsNow(address a) public view returns (uint256 p) { if (seed.balanceOf(a) == 0) return 0; uint8 lv = seed.levelOfHolder(a); if (lv == 0) return 0; address src = seed.shares(); if (src.code.length == 0) return 0; try INectarShares(src).sharesOf(a) returns (uint256 s) { p = s > MAX_SHARES_COUNTED ? MAX_SHARES_COUNTED : s; } catch { return 0; } if (lv == 2) p *= 2; } /// @notice Record today's points for these wallets. The first check-in of the day counts. function checkIn(address[] calldata who) external { uint256 d = today(); uint256 added; for (uint256 i; i < who.length; ++i) { address a = who[i]; if (pointsOf[d][a] != 0) continue; uint256 p = pointsNow(a); if (p == 0) continue; pointsOf[d][a] = p; added += p; emit CheckedIn(d, a, p); } totalPoints[d] += added; } function claimable(address a, uint256[] calldata dayList) external view returns (uint256 sum) { uint256 d = today(); for (uint256 i; i < dayList.length; ++i) sum += _share(a, dayList[i], d); } function _share(address a, uint256 day, uint256 nowDay) private view returns (uint256) { if (day >= nowDay || claimed[day][a]) return 0; uint256 p = pointsOf[day][a]; if (p == 0) return 0; return pot[day] * p / totalPoints[day]; } /// @notice Collect your share of finished days. Listing a day twice or a day with nothing is harmless. function claim(uint256[] calldata dayList) external returns (uint256 sum) { if (_lock != 1) revert Reentrancy(); _lock = 2; uint256 d = today(); for (uint256 i; i < dayList.length; ++i) { uint256 amt = _share(msg.sender, dayList[i], d); if (amt == 0) continue; claimed[dayList[i]][msg.sender] = true; sum += amt; } if (sum != 0) { totalClaimed += sum; (bool ok,) = msg.sender.call{value: sum}(""); if (!ok) revert TransferFailed(); emit Claimed(msg.sender, sum); } _lock = 1; } /// @notice A finished day that had HYPE but nobody checked in moves into today's pot. function roll(uint256 day) external { uint256 d = today(); if (day >= d || totalPoints[day] != 0) return; uint256 amt = pot[day]; if (amt == 0) return; pot[day] = 0; pot[d] += amt; emit Rolled(day, d, amt); } }