// SPDX-License-Identifier: MIT pragma solidity ^0.8.28; import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; import {IPonsFactory, IPonsCurve} from "./IPons.sol"; /// @title SightlineLauncher /// @notice Launches event tokens on pons (Robinhood Chain's bonding-curve launchpad) /// and remembers which real-world event each one is pinned to. /// /// Every token launched here carries Sightline's signature: /// - it is deployed by this contract, so pons records Sightline as the launcher; /// - its onchain website link is https://trysightline.io. /// Everything else follows pons: the creator (msg.sender) is the creator fee /// recipient and chooses the creator tax; trading and graduation happen on pons. /// /// This contract holds no funds between transactions and has no owner. contract SightlineLauncher is ReentrancyGuard { struct LaunchInput { string name; string symbol; string logo; // image URI (https:// or ipfs://), optional string description; string twitter; // optional creator socials string telegram; uint16 creatorTaxBps; // extra creator fee on trades, capped by pons bool buybackEnabled; uint256 launchConfigId; bytes32 expectedEconomics; // pons.previewLaunchEconomics(launchConfigId, ETH); guards against config changes // Where and what on Sightline's globe. string category; string eventId; int32 latE6; int32 lngE6; } struct Launch { address token; address curve; address creator; uint64 createdAt; uint64 createdBlock; int32 latE6; int32 lngE6; string category; string eventId; } string public constant WEBSITE = "https://trysightline.io"; uint256 public constant MAX_EVENT_ID_LENGTH = 128; uint256 public constant MAX_CATEGORY_LENGTH = 24; /// pons' marker for a native-ETH quote. address internal constant ETH = address(0); IPonsFactory public immutable pons; Launch[] private _launches; mapping(address token => uint256 indexPlusOne) private _index; mapping(address creator => uint256) public launchNonce; event Launched( address indexed token, address indexed creator, uint256 indexed index, address curve, string eventId ); error InsufficientLaunchFee(); error BadEvent(); error BadCoordinates(); error UnknownToken(); error RefundFailed(); constructor(IPonsFactory pons_) { pons = pons_; } /// @notice Launch a token on pons for a real-world event. Send the pons launch fee, /// plus any ETH to buy with in the same transaction (the creator's dev buy, which /// pons exempts from its anti-sniping tax). function launch(LaunchInput calldata p, uint256 minTokensOut) external payable nonReentrant returns (address token, address curve, uint256 tokensOut) { uint256 eventIdLen = bytes(p.eventId).length; uint256 categoryLen = bytes(p.category).length; if (eventIdLen == 0 || eventIdLen > MAX_EVENT_ID_LENGTH || categoryLen == 0 || categoryLen > MAX_CATEGORY_LENGTH) { revert BadEvent(); } if (p.latE6 < -90_000_000 || p.latE6 > 90_000_000 || p.lngE6 < -180_000_000 || p.lngE6 > 180_000_000) { revert BadCoordinates(); } uint256 fee = pons.launchFee(); if (msg.value < fee) revert InsufficientLaunchFee(); (token, curve) = pons.launchToken{value: fee}(_tokenParams(p), p.launchConfigId, ETH, new address[](0)); uint256 devBuy = msg.value - fee; if (devBuy > 0) tokensOut = IPonsCurve(curve).buy{value: devBuy}(devBuy, minTokensOut, msg.sender); _launches.push( Launch({ token: token, curve: curve, creator: msg.sender, createdAt: uint64(block.timestamp), createdBlock: uint64(block.number), latE6: p.latE6, lngE6: p.lngE6, category: p.category, eventId: p.eventId }) ); _index[token] = _launches.length; emit Launched(token, msg.sender, _launches.length - 1, curve, p.eventId); // If the curve hands back unspent ETH (e.g. it filled up mid-buy), pass it on. uint256 leftover = address(this).balance; if (leftover > 0) { (bool ok,) = msg.sender.call{value: leftover}(""); if (!ok) revert RefundFailed(); } } function _tokenParams(LaunchInput calldata p) internal returns (IPonsFactory.TokenParams memory t) { t.name = p.name; t.symbol = p.symbol; t.logo = p.logo; t.description = p.description; t.socials.twitter = p.twitter; t.socials.telegram = p.telegram; t.socials.website = WEBSITE; t.creatorFeeRecipient = msg.sender; t.creatorTaxBps = p.creatorTaxBps; t.buybackEnabled = p.buybackEnabled; t.expectedEconomics = p.expectedEconomics; // pons namespaces salts by launcher, so every Sightline launch shares one namespace. // Deriving the salt from the creator and their nonce keeps launches from colliding // and stops anyone from squatting another creator's next address. t.salt = keccak256(abi.encode(msg.sender, launchNonce[msg.sender]++)); } // ----------------------------------------------------------------- views function launchCount() external view returns (uint256) { return _launches.length; } function getLaunch(address token) external view returns (Launch memory) { uint256 i = _index[token]; if (i == 0) revert UnknownToken(); return _launches[i - 1]; } /// @notice Page through launches, oldest first. Returns fewer than `limit` at the end. function getLaunches(uint256 offset, uint256 limit) external view returns (Launch[] memory page) { uint256 total = _launches.length; if (offset >= total) return new Launch[](0); uint256 end = offset + limit > total ? total : offset + limit; page = new Launch[](end - offset); for (uint256 i = offset; i < end; i++) { page[i - offset] = _launches[i]; } } /// Receives refunds from pons curves during a launch. receive() external payable {} }