// SPDX-License-Identifier: MIT pragma solidity ^0.8.26; struct PoolKey { address currency0; address currency1; uint24 fee; int24 tickSpacing; address hooks; } interface IERC20 { function balanceOf(address) external view returns (uint256); function approve(address,uint256) external returns (bool); function transfer(address,uint256) external returns (bool); function transferFrom(address,address,uint256) external returns (bool); } interface IPositions { function poolManager() external view returns(address); function permit2() external view returns(address); function initializePool(PoolKey calldata,uint160) external payable returns(int24); function nextTokenId() external view returns(uint256); function ownerOf(uint256) external view returns(address); function getPositionLiquidity(uint256) external view returns(uint128); function modifyLiquidities(bytes calldata,uint256) external payable; } interface IPermit2 { function approve(address,address,uint160,uint48) external; } interface IStateView { function poolManager() external view returns(address); function getSlot0(bytes32) external view returns(uint160,int24,uint24,uint24); } /// @notice Custodies v4 LP positions. No administrator can transfer LP principal. /// @dev Deployment candidate: requires integration verification before admission. contract RebloomVault { address public immutable administrator; address public immutable platformRecipient; IPositions public immutable positionManager; IPermit2 public immutable permit2; IStateView public immutable stateView; address public immutable poolManager; mapping(address=>bool) public stockTokens; mapping(address=>bytes32) public admittedOldCodeHash; bool public depositsPaused; bool public projectClosed; // Collected fees stay reserved through allocation until actually paid mapping(address=>uint256) public reservedTokenBalances; uint256 private entered = 1; struct Position { address contributor; address token0; address token1; bytes32 poolId; uint128 liquidity; bool permanent; } mapping(uint256=>Position) public positions; mapping(bytes32=>mapping(address=>uint256)) public communityFees; mapping(address=>uint256) public platformFees; struct Epoch { bytes32 poolId; address token; bytes32 root; uint256 total; uint256 claimed; uint64 opensAt; bool cancelled; } uint256 public nextEpoch = 1; mapping(uint256=>Epoch) public epochs; mapping(uint256=>mapping(uint256=>uint256)) private claimedWords; event Deposited(uint256 indexed tokenId,bytes32 indexed poolId,address indexed contributor,uint128 liquidity,uint256 amount0,uint256 amount1,bool permanent); event Withdrawn(uint256 indexed tokenId,address indexed contributor,uint256 amount0,uint256 amount1); event FeesCollected(uint256 indexed tokenId,address indexed token,uint256 community,uint256 platform); event OldTokenAdmission(address indexed token,bytes32 codeHash); event PauseChanged(bool paused); event ProjectClosed(); event SurplusRecovered(address indexed token,address indexed recipient,uint256 amount); event EpochPublished(uint256 indexed epoch,bytes32 indexed poolId,address indexed token,bytes32 root,uint256 total,uint64 opensAt); event EpochCancelled(uint256 indexed epoch); event Claimed(uint256 indexed epoch,uint256 indexed index,address indexed recipient,uint256 amount); error Invalid(); error Unauthorized(); error Locked(); error Expired(); error PriceMoved(); error TransferFailed(); error Busy(); modifier nonReentrant(){if(entered!=1)revert Busy();entered=2;_;entered=1;} modifier onlyAdmin(){if(msg.sender!=administrator)revert Unauthorized();_;} constructor(address admin,address platform,address pos,address permit,address viewAddress,address manager,address[3] memory stocks,address initialOld){ if(admin==address(0)||platform==address(0)||pos.code.length==0||permit.code.length==0||viewAddress.code.length==0||manager.code.length==0)revert Invalid(); administrator=admin;platformRecipient=platform;positionManager=IPositions(pos);permit2=IPermit2(permit);stateView=IStateView(viewAddress);poolManager=manager; if(IPositions(pos).poolManager()!=manager||IPositions(pos).permit2()!=permit||IStateView(viewAddress).poolManager()!=manager)revert Invalid(); for(uint256 i;i<3;i++){if(stocks[i].code.length==0||stockTokens[stocks[i]])revert Invalid();stockTokens[stocks[i]]=true;} if(initialOld.code.length==0||stockTokens[initialOld])revert Invalid(); admittedOldCodeHash[initialOld]=initialOld.codehash;emit OldTokenAdmission(initialOld,initialOld.codehash); } function setOldToken(address token,bool admitted) external onlyAdmin { if(admitted&&(token.code.length==0||stockTokens[token]))revert Invalid(); admittedOldCodeHash[token]=admitted?token.codehash:bytes32(0);emit OldTokenAdmission(token,admittedOldCodeHash[token]); } receive() external payable {} function setDepositsPaused(bool paused) external onlyAdmin {if(projectClosed&&!paused)revert Locked();depositsPaused=paused;emit PauseChanged(paused);} function closeProject() external onlyAdmin {if(projectClosed)revert Locked();projectClosed=true;depositsPaused=true;emit PauseChanged(true);emit ProjectClosed();} function recoverableTokenBalance(address token) public view returns(uint256){ uint256 balance=_balance(token);uint256 reserved=reservedTokenBalances[token];return balance>reserved?balance-reserved:0; } function recoverSurplusToken(address token,uint256 amount) external onlyAdmin nonReentrant { if(amount==0||amount>recoverableTokenBalance(token))revert Invalid(); uint256 beforeBalance=_balance(token);_send(token,platformRecipient,amount); if(_balance(token)!=beforeBalance-amount)revert TransferFailed(); emit SurplusRecovered(token,platformRecipient,amount); } function recoverNative(uint256 amount) external onlyAdmin nonReentrant { if(amount==0||amount>address(this).balance)revert Invalid(); (bool ok,)=payable(platformRecipient).call{value:amount}("");if(!ok)revert TransferFailed(); emit SurplusRecovered(address(0),platformRecipient,amount); } function _deadline(uint256 deadline) private view {if(deadlineblock.timestamp+20 minutes)revert Expired();} function _call(address token,bytes memory data) private { (bool ok,bytes memory result)=token.call(data);if(!ok||(result.length!=0&&(result.length!=32||!abi.decode(result,(bool)))))revert TransferFailed(); } function _balance(address token) private view returns(uint256){return IERC20(token).balanceOf(address(this));} function _send(address token,address to,uint256 amount) private {if(amount==0)return;uint256 beforeBalance=IERC20(token).balanceOf(to);_call(token,abi.encodeCall(IERC20.transfer,(to,amount)));if(IERC20(token).balanceOf(to)-beforeBalance!=amount)revert TransferFailed();} function _pull(address token,uint256 amount) private {uint256 beforeBalance=_balance(token);_call(token,abi.encodeCall(IERC20.transferFrom,(msg.sender,address(this),amount)));if(_balance(token)-beforeBalance!=amount)revert TransferFailed();} function _approve(address token,uint256 amount,uint48 deadline) private { _call(token,abi.encodeCall(IERC20.approve,(address(permit2),0))); if(amount!=0)_call(token,abi.encodeCall(IERC20.approve,(address(permit2),amount))); permit2.approve(token,address(positionManager),uint160(amount),deadline); } struct Deposit { PoolKey key; uint160 initialSqrtPriceX96; uint160 minSqrtPriceX96; uint160 maxSqrtPriceX96; uint128 liquidity; uint128 amount0Max; uint128 amount1Max; uint256 deadline; bool permanent; } function deposit(Deposit calldata d) external nonReentrant returns(uint256 id){ _deadline(d.deadline); if(depositsPaused||d.liquidity==0||d.liquidity>uint128(type(int128).max)||d.amount0Max==0||d.amount1Max==0)revert Invalid(); PoolKey calldata key=d.key; if(key.currency0>=key.currency1||key.fee!=3000||key.tickSpacing!=60||key.hooks!=address(0))revert Invalid(); bool s0=stockTokens[key.currency0];bool s1=stockTokens[key.currency1];if(s0==s1)revert Invalid(); address old=s0?key.currency1:key.currency0; if(admittedOldCodeHash[old]==bytes32(0)||admittedOldCodeHash[old]!=old.codehash)revert Unauthorized(); // Full-range positions with both tokens; reject inactive boundary positions. if(d.minSqrtPriceX96<=4306310044||d.maxSqrtPriceX96>=1457652066949847389969617340386294118487833376468||d.minSqrtPriceX96>d.maxSqrtPriceX96)revert Invalid(); bytes32 poolId=keccak256(abi.encode(key)); (uint160 price,,,)=stateView.getSlot0(poolId); if(price==0){if(d.initialSqrtPriceX96d.maxSqrtPriceX96)revert Invalid();positionManager.initializePool(key,d.initialSqrtPriceX96);} uint256 base0=_balance(key.currency0);uint256 base1=_balance(key.currency1); _pull(key.currency0,d.amount0Max);_pull(key.currency1,d.amount1Max); _approve(key.currency0,d.amount0Max,uint48(d.deadline));_approve(key.currency1,d.amount1Max,uint48(d.deadline)); (price,,,)=stateView.getSlot0(poolId);if(priced.maxSqrtPriceX96)revert PriceMoved(); id=positionManager.nextTokenId(); bytes[] memory params=new bytes[](2); params[0]=abi.encode(key,int24(-887220),int24(887220),uint256(d.liquidity),d.amount0Max,d.amount1Max,address(this),bytes("")); params[1]=abi.encode(key.currency0,key.currency1); positionManager.modifyLiquidities(abi.encode(hex"020d",params),d.deadline); if(positionManager.ownerOf(id)!=address(this)||positionManager.getPositionLiquidity(id)!=d.liquidity)revert Invalid(); _approve(key.currency0,0,0);_approve(key.currency1,0,0); uint256 refund0=_balance(key.currency0)-base0;uint256 refund1=_balance(key.currency1)-base1; if(refund0>d.amount0Max||refund1>d.amount1Max)revert Invalid(); positions[id]=Position(msg.sender,key.currency0,key.currency1,poolId,d.liquidity,d.permanent); _send(key.currency0,msg.sender,refund0);_send(key.currency1,msg.sender,refund1); emit Deposited(id,poolId,msg.sender,d.liquidity,d.amount0Max-refund0,d.amount1Max-refund1,d.permanent); } function _decrease(uint256 id,uint128 liquidity,uint128 min0,uint128 min1,uint256 deadline) private { Position storage p=positions[id];bytes[] memory params=new bytes[](2); params[0]=abi.encode(id,uint256(liquidity),min0,min1,bytes(""));params[1]=abi.encode(p.token0,p.token1,address(this)); positionManager.modifyLiquidities(abi.encode(hex"0111",params),deadline); } function _credit(uint256 id,address token,uint256 amount) private { // Round the platform portion down so dust remains with the community. uint256 platform=amount/10;communityFees[positions[id].poolId][token]+=amount-platform;platformFees[token]+=platform; reservedTokenBalances[token]+=amount;emit FeesCollected(id,token,amount-platform,platform); } function _collect(uint256 id) private { Position storage p=positions[id];if(p.contributor==address(0))revert Invalid(); uint256 before0=_balance(p.token0);uint256 before1=_balance(p.token1); _decrease(id,0,0,0,block.timestamp);_credit(id,p.token0,_balance(p.token0)-before0);_credit(id,p.token1,_balance(p.token1)-before1); } function collect(uint256 id) external nonReentrant {_collect(id);} function withdraw(uint256 id,uint128 min0,uint128 min1,uint256 deadline) external nonReentrant { _deadline(deadline);Position storage p=positions[id];if(msg.sender!=p.contributor)revert Unauthorized();if(p.permanent)revert Locked();if(p.liquidity==0)revert Invalid(); _collect(id);uint256 before0=_balance(p.token0);uint256 before1=_balance(p.token1);uint128 liquidity=p.liquidity;p.liquidity=0; _decrease(id,liquidity,min0,min1,deadline); uint256 amount0=_balance(p.token0)-before0;uint256 amount1=_balance(p.token1)-before1; _send(p.token0,msg.sender,amount0);_send(p.token1,msg.sender,amount1);emit Withdrawn(id,msg.sender,amount0,amount1); } function payPlatform(address token) external nonReentrant {uint256 amount=platformFees[token];platformFees[token]=0;reservedTokenBalances[token]-=amount;_send(token,platformRecipient,amount);} // Community allocation is a disclosed, reviewable publisher role; it never controls LP principal. function publishEpoch(bytes32 poolId,address token,bytes32 root,uint256 total) external onlyAdmin returns(uint256 id){ if(root==bytes32(0)||total==0||total>communityFees[poolId][token])revert Invalid();communityFees[poolId][token]-=total; id=nextEpoch++;uint64 opensAt=uint64(block.timestamp+48 hours);epochs[id]=Epoch(poolId,token,root,total,0,opensAt,false); emit EpochPublished(id,poolId,token,root,total,opensAt); } function cancelEpoch(uint256 id) external onlyAdmin {Epoch storage e=epochs[id];if(e.root==bytes32(0)||e.cancelled||block.timestamp>=e.opensAt)revert Invalid();e.cancelled=true;communityFees[e.poolId][e.token]+=e.total;emit EpochCancelled(id);} function claim(uint256 id,uint256 index,address recipient,uint256 amount,bytes32[] calldata proof) external nonReentrant { Epoch storage e=epochs[id];if(e.root==bytes32(0)||e.cancelled||block.timestampe.total)revert Invalid(); uint256 word=index>>8;uint256 mask=1<<(index&255);if(claimedWords[id][word]&mask!=0)revert Invalid(); bytes32 h=keccak256(bytes.concat(keccak256(abi.encode(block.chainid,address(this),id,e.poolId,e.token,index,recipient,amount)))); for(uint256 i;i