// SPDX-License-Identifier: MIT pragma solidity ^0.8.26; interface IPonsEscrow { function claim() external; } interface IPonsFactory { function feeEscrow() external view returns(address); } /// @notice Creator-reviewed ETH bounties. No upgrade or admin withdrawal of community funds. /// @dev Experimental. A creator decides which submissions qualify; this is not arbitration. contract BountideVault { address public immutable owner; address public immutable escrow; uint16 public immutable communityBps; uint256 public available; uint256 public creatorBalance; uint256 public reserved; uint256 public totalPaid; uint256 public bountyCount; uint256 private entered; struct Bounty { string title; string brief; uint256 reward; uint64 deadline; uint8 status; address winner; uint256 submissions; } mapping(uint256 => Bounty) private bounties; mapping(uint256 => mapping(address => string)) public submissions; mapping(uint256 => address[]) private contributors; event BountyOpened(uint256 indexed id, string title, uint256 reward, uint64 deadline); event Submission(uint256 indexed id, address indexed contributor, string proof); event Awarded(uint256 indexed id, address indexed contributor, uint256 reward); event Expired(uint256 indexed id); event Funded(address indexed sender, uint256 communityAmount, uint256 creatorAmount); error Unauthorized(); error InvalidInput(); error InvalidState(); error InsufficientPool(); error TransferFailed(); error Reentrancy(); modifier onlyOwner(){ if(msg.sender!=owner)revert Unauthorized(); _; } modifier guard(){ if(entered!=0)revert Reentrancy(); entered=1; _; entered=0; } constructor(address factory, uint16 share){ if(share>10000 || share<1000)revert InvalidInput(); owner=msg.sender; communityBps=share; escrow=IPonsFactory(factory).feeEscrow(); if(escrow==address(0) || escrow.code.length==0)revert InvalidInput(); } receive() external payable { uint256 creator=msg.sender==escrow ? msg.value-(msg.value*communityBps/10000) : 0; available+=msg.value-creator; creatorBalance+=creator; emit Funded(msg.sender,msg.value-creator,creator); } function collectFees() external guard { IPonsEscrow(escrow).claim(); } function withdrawCreator() external onlyOwner guard { uint256 amount=creatorBalance; if(amount==0)revert InvalidState(); creatorBalance=0; (bool ok,)=owner.call{value:amount}(""); if(!ok)revert TransferFailed(); } function getBounty(uint256 id) external view returns(Bounty memory){return bounties[id];} function getContributors(uint256 id,uint256 offset) external view returns(address[] memory result){ address[] storage list=contributors[id]; if(offset>=list.length)return new address[](0); uint256 end=offset+50; if(end>list.length)end=list.length; result=new address[](end-offset);for(uint256 i=offset;i120 || bytes(brief).length<20 || bytes(brief).length>2048 || reward==0 || deadlineblock.timestamp+180 days)revert InvalidInput(); if(reward>available)revert InsufficientPool(); available-=reward;reserved+=reward; uint256 id=++bountyCount; bounties[id]=Bounty(title,brief,reward,deadline,1,address(0),0); emit BountyOpened(id,title,reward,deadline); } function submit(uint256 id,string calldata proof) external { Bounty storage b=bounties[id]; if(b.status!=1 || block.timestamp>b.deadline)revert InvalidState(); if(bytes(proof).length<8 || bytes(proof).length>512)revert InvalidInput(); if(bytes(submissions[id][msg.sender]).length==0){b.submissions++;contributors[id].push(msg.sender);} submissions[id][msg.sender]=proof; emit Submission(id,msg.sender,proof); } function award(uint256 id,address payable contributor) external onlyOwner guard { Bounty storage b=bounties[id]; if(b.status!=1 || block.timestamp>uint256(b.deadline)+7 days || bytes(submissions[id][contributor]).length==0)revert InvalidState(); b.status=2;b.winner=contributor;reserved-=b.reward;totalPaid+=b.reward; (bool ok,)=contributor.call{value:b.reward}(""); if(!ok)revert TransferFailed(); emit Awarded(id,contributor,b.reward); } function expire(uint256 id) external { Bounty storage b=bounties[id]; if(b.status!=1 || block.timestamp<=uint256(b.deadline)+7 days)revert InvalidState(); b.status=3;reserved-=b.reward;available+=b.reward;emit Expired(id); } }