// SPDX-License-Identifier: MIT pragma solidity 0.8.28; struct PoolKey { address currency0; address currency1; uint24 fee; int24 tickSpacing; address hooks; } interface IArcAssay { function assay(PoolKey calldata key, uint256 usdcIn) external payable returns (uint8 verdict, uint32 lossBps); function fee() external view returns (uint256); } /// @title Assay Bond — a deployer prepays a run of assays for their pool; a failed mark sends the rest to the faucet pot /// @notice Anyone may post a bond for a pool: USDC that pays one assay every `interval` seconds until it runs out. Anyone may call /// `keep` when an assay is due: the caller is refunded a fixed gas allowance from the bond, the assay fee goes to Touchstone /// (and from there to the faucet pot and the APEX vault), and the mark is written on chain. If a mark says the token cannot be /// sold, the remaining bond is sent to the faucet pot at once. The poster can never take a bond back. A rug pays the poor. contract AssayBond { IArcAssay public immutable assayer; address payable public immutable faucetPot; uint256 public constant MIN_BOND = 0.5 ether; uint256 public constant MIN_INTERVAL = 10 minutes; uint256 public constant KEEPER_GAS = 0.03 ether; struct Bond { PoolKey key; address poster; uint256 remaining; uint64 interval; uint64 lastAssay; uint32 marks; uint32 okMarks; bool failed; } Bond[] public bonds; mapping(bytes32 => uint256[]) public bondsOf; event Posted(uint256 indexed id, bytes32 indexed poolId, address indexed poster, uint256 amount, uint64 interval); event Kept(uint256 indexed id, address indexed keeper, uint8 verdict, uint32 lossBps, uint256 remaining); event Forfeited(uint256 indexed id, uint256 toPot); constructor(address assayer_, address payable faucetPot_) { assayer = IArcAssay(assayer_); faucetPot = faucetPot_; } function count() external view returns (uint256) { return bonds.length; } function poolId(PoolKey calldata key) public pure returns (bytes32) { return keccak256(abi.encode(key)); } function post(PoolKey calldata key, uint64 interval) external payable returns (uint256 id) { require(msg.value >= MIN_BOND, "min 0.5 USDC"); require(interval >= MIN_INTERVAL, "interval"); id = bonds.length; bonds.push(Bond(key, msg.sender, msg.value, interval, 0, 0, 0, false)); bondsOf[poolId(key)].push(id); emit Posted(id, poolId(key), msg.sender, msg.value, interval); } function due(uint256 id) public view returns (bool) { Bond storage b = bonds[id]; return !b.failed && b.remaining >= assayer.fee() + KEEPER_GAS && block.timestamp >= b.lastAssay + b.interval; } function keep(uint256 id) external { require(due(id), "not due"); Bond storage b = bonds[id]; uint256 fee = assayer.fee(); b.remaining -= fee + KEEPER_GAS; b.lastAssay = uint64(block.timestamp); b.marks += 1; (uint8 verdict, uint32 lossBps) = assayer.assay{value: fee}(b.key, 0); if (verdict == 1) b.okMarks += 1; (bool ok, ) = payable(msg.sender).call{value: KEEPER_GAS}(""); require(ok, "keeper pay"); emit Kept(id, msg.sender, verdict, lossBps, b.remaining); if (verdict != 1 && b.remaining > 0) { uint256 rest = b.remaining; b.remaining = 0; b.failed = true; (bool ok2, ) = faucetPot.call{value: rest}(""); require(ok2, "pot"); emit Forfeited(id, rest); } } }