Contracts · MIT license
SightlineLauncher.v2.sol
Launcher v2 exactly as deployed: STLN names and per-token Sightline links.
1// SPDX-License-Identifier: MIT2pragma solidity ^0.8.28;3 4import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";5import {Strings} from "@openzeppelin/contracts/utils/Strings.sol";6import {IPonsFactory, IPonsCurve} from "./IPons.sol";7 8/// Arbitrum's system contract; on Arbitrum chains `block.number` is the L1 block.9interface IArbSys {10 function arbBlockNumber() external view returns (uint256);11}12 13/// @title SightlineLauncher14/// @notice Launches event tokens on pons (Robinhood Chain's bonding-curve launchpad)15/// and remembers which real-world event each one is pinned to.16///17/// Every token launched here carries Sightline's signature:18/// - its name starts with "STLN - ";19/// - it is deployed by this contract, so pons records Sightline as the launcher;20/// - its onchain website is its own Sightline page, https://trysightline.io/l/<index>.21/// Everything else follows pons: the creator (msg.sender) is the creator fee22/// recipient and chooses the creator tax; trading and graduation happen on pons.23///24/// This contract holds no funds between transactions and has no owner.25contract SightlineLauncher is ReentrancyGuard {26 struct LaunchInput {27 string name; // without the "STLN - " prefix, which is added here28 string symbol;29 string logo; // image URI (https:// or ipfs://), optional30 string description;31 string twitter; // optional creator socials32 string telegram;33 uint16 creatorTaxBps; // extra creator fee on trades, capped by pons34 bool buybackEnabled;35 uint256 launchConfigId;36 bytes32 expectedEconomics; // pons.previewLaunchEconomics(launchConfigId, ETH); guards against config changes37 // Where and what on Sightline's globe.38 string category;39 string eventId;40 int32 latE6;41 int32 lngE6;42 }43 44 struct Launch {45 address token;46 address curve;47 address creator;48 uint64 createdAt;49 uint64 createdBlock;50 int32 latE6;51 int32 lngE6;52 string category;53 string eventId;54 }55 56 string public constant WEBSITE = "https://trysightline.io";57 string public constant NAME_PREFIX = "STLN - ";58 uint256 public constant MAX_NAME_LENGTH = 48;59 uint256 public constant MAX_EVENT_ID_LENGTH = 128;60 uint256 public constant MAX_CATEGORY_LENGTH = 24;61 /// pons' marker for a native-ETH quote.62 address internal constant ETH = address(0);63 64 IPonsFactory public immutable pons;65 66 Launch[] private _launches;67 mapping(address token => uint256 indexPlusOne) private _index;68 mapping(address creator => uint256) public launchNonce;69 70 event Launched(71 address indexed token, address indexed creator, uint256 indexed index, address curve, string eventId72 );73 74 error InsufficientLaunchFee();75 error BadName();76 error BadEvent();77 error BadCoordinates();78 error UnknownToken();79 error RefundFailed();80 81 constructor(IPonsFactory pons_) {82 pons = pons_;83 }84 85 /// @notice Launch a token on pons for a real-world event. Send the pons launch fee,86 /// plus any ETH to buy with in the same transaction (the creator's dev buy, which87 /// pons exempts from its anti-sniping tax).88 function launch(LaunchInput calldata p, uint256 minTokensOut)89 external90 payable91 nonReentrant92 returns (address token, address curve, uint256 tokensOut)93 {94 uint256 nameLen = bytes(p.name).length;95 if (nameLen == 0 || nameLen > MAX_NAME_LENGTH) revert BadName();96 uint256 eventIdLen = bytes(p.eventId).length;97 uint256 categoryLen = bytes(p.category).length;98 if (eventIdLen == 0 || eventIdLen > MAX_EVENT_ID_LENGTH || categoryLen == 0 || categoryLen > MAX_CATEGORY_LENGTH) {99 revert BadEvent();100 }101 if (p.latE6 < -90_000_000 || p.latE6 > 90_000_000 || p.lngE6 < -180_000_000 || p.lngE6 > 180_000_000) {102 revert BadCoordinates();103 }104 uint256 fee = pons.launchFee();105 if (msg.value < fee) revert InsufficientLaunchFee();106 107 (token, curve) =108 pons.launchToken{value: fee}(_tokenParams(p), p.launchConfigId, ETH, new address[](0));109 110 uint256 devBuy = msg.value - fee;111 if (devBuy > 0) tokensOut = IPonsCurve(curve).buy{value: devBuy}(devBuy, minTokensOut, msg.sender);112 113 _launches.push(114 Launch({115 token: token,116 curve: curve,117 creator: msg.sender,118 createdAt: uint64(block.timestamp),119 createdBlock: uint64(_blockNumber()),120 latE6: p.latE6,121 lngE6: p.lngE6,122 category: p.category,123 eventId: p.eventId124 })125 );126 _index[token] = _launches.length;127 emit Launched(token, msg.sender, _launches.length - 1, curve, p.eventId);128 129 // If the curve hands back unspent ETH (e.g. it filled up mid-buy), pass it on.130 uint256 leftover = address(this).balance;131 if (leftover > 0) {132 (bool ok,) = msg.sender.call{value: leftover}("");133 if (!ok) revert RefundFailed();134 }135 }136 137 function _tokenParams(LaunchInput calldata p) internal returns (IPonsFactory.TokenParams memory t) {138 t.name = string.concat(NAME_PREFIX, p.name);139 t.symbol = p.symbol;140 t.logo = p.logo;141 t.description = p.description;142 t.socials.twitter = p.twitter;143 t.socials.telegram = p.telegram;144 // The launch index is known before the token address: /l/<index> redirects to the token page.145 t.socials.website = string.concat(WEBSITE, "/l/", Strings.toString(_launches.length));146 t.creatorFeeRecipient = msg.sender;147 t.creatorTaxBps = p.creatorTaxBps;148 t.buybackEnabled = p.buybackEnabled;149 t.expectedEconomics = p.expectedEconomics;150 // pons namespaces salts by launcher, so every Sightline launch shares one namespace.151 // Deriving the salt from the creator and their nonce keeps launches from colliding152 // and stops anyone from squatting another creator's next address.153 t.salt = keccak256(abi.encode(msg.sender, launchNonce[msg.sender]++));154 }155 156 /// The chain's own block number: ArbSys on Arbitrum chains, block.number elsewhere.157 function _blockNumber() internal view returns (uint256) {158 if (address(100).code.length == 0) return block.number;159 try IArbSys(address(100)).arbBlockNumber() returns (uint256 n) {160 return n;161 } catch {162 return block.number;163 }164 }165 166 // ----------------------------------------------------------------- views167 168 function launchCount() external view returns (uint256) {169 return _launches.length;170 }171 172 function getLaunch(address token) external view returns (Launch memory) {173 uint256 i = _index[token];174 if (i == 0) revert UnknownToken();175 return _launches[i - 1];176 }177 178 /// @notice Page through launches, oldest first. Returns fewer than `limit` at the end.179 function getLaunches(uint256 offset, uint256 limit) external view returns (Launch[] memory page) {180 uint256 total = _launches.length;181 if (offset >= total) return new Launch[](0);182 uint256 end = offset + limit > total ? total : offset + limit;183 page = new Launch[](end - offset);184 for (uint256 i = offset; i < end; i++) {185 page[i - offset] = _launches[i];186 }187 }188 189 /// Receives refunds from pons curves during a launch.190 receive() external payable {}191}