Contracts · MIT license
SightlineLauncher.sol
Launches tokens on pons with Sightline's signature, the creator's snipe-tax whitelist, and each token's event and place (v3).
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/// The creator can name up to 30 wallets that skip pons' anti-sniping tax at launch.22/// Everything else follows pons: the creator (msg.sender) is the creator fee23/// recipient and chooses the creator tax; trading and graduation happen on pons.24///25/// This contract holds no funds between transactions and has no owner.26contract SightlineLauncher is ReentrancyGuard {27 struct LaunchInput {28 string name; // without the "STLN - " prefix, which is added here29 string symbol;30 string logo; // image URI (https:// or ipfs://), optional31 string description;32 string twitter; // optional creator socials33 string telegram;34 uint16 creatorTaxBps; // extra creator fee on trades, capped by pons35 bool buybackEnabled;36 uint256 launchConfigId;37 bytes32 expectedEconomics; // pons.previewLaunchEconomics(launchConfigId, ETH); guards against config changes38 // Where and what on Sightline's globe.39 string category;40 string eventId;41 int32 latE6;42 int32 lngE6;43 }44 45 struct Launch {46 address token;47 address curve;48 address creator;49 uint64 createdAt;50 uint64 createdBlock;51 int32 latE6;52 int32 lngE6;53 string category;54 string eventId;55 }56 57 string public constant WEBSITE = "https://trysightline.io";58 string public constant NAME_PREFIX = "STLN - ";59 uint256 public constant MAX_NAME_LENGTH = 48;60 uint256 public constant MAX_EVENT_ID_LENGTH = 128;61 /// pons exempts at most 32 addresses per launch; the launcher and the creator are62 /// exempt automatically, which leaves 30 for the creator to choose.63 uint256 public constant MAX_SNIPE_EXEMPTIONS = 30;64 uint256 public constant MAX_CATEGORY_LENGTH = 24;65 /// pons' marker for a native-ETH quote.66 address internal constant ETH = address(0);67 68 IPonsFactory public immutable pons;69 70 Launch[] private _launches;71 mapping(address token => uint256 indexPlusOne) private _index;72 mapping(address creator => uint256) public launchNonce;73 74 event Launched(75 address indexed token, address indexed creator, uint256 indexed index, address curve, string eventId76 );77 78 error InsufficientLaunchFee();79 error BadName();80 error TooManyExemptions();81 error BadEvent();82 error BadCoordinates();83 error UnknownToken();84 error RefundFailed();85 86 constructor(IPonsFactory pons_) {87 pons = pons_;88 }89 90 /// @notice Launch a token on pons for a real-world event. Send the pons launch fee,91 /// plus any ETH to buy with in the same transaction (the creator's dev buy, which92 /// pons exempts from its anti-sniping tax).93 /// @param snipeTaxExempt wallets that may buy in the first seconds without pons'94 /// anti-sniping tax (e.g. the creator's other wallets), at most MAX_SNIPE_EXEMPTIONS.95 function launch(LaunchInput calldata p, address[] calldata snipeTaxExempt, uint256 minTokensOut)96 external97 payable98 nonReentrant99 returns (address token, address curve, uint256 tokensOut)100 {101 if (snipeTaxExempt.length > MAX_SNIPE_EXEMPTIONS) revert TooManyExemptions();102 _validate(p);103 uint256 fee = pons.launchFee();104 if (msg.value < fee) revert InsufficientLaunchFee();105 106 (token, curve) =107 pons.launchToken{value: fee}(_tokenParams(p), p.launchConfigId, ETH, snipeTaxExempt);108 109 if (msg.value > fee) tokensOut = IPonsCurve(curve).buy{value: msg.value - fee}(msg.value - fee, minTokensOut, msg.sender);110 111 _record(p, token, curve);112 _refundLeftover();113 }114 115 function _validate(LaunchInput calldata p) internal pure {116 uint256 nameLen = bytes(p.name).length;117 if (nameLen == 0 || nameLen > MAX_NAME_LENGTH) revert BadName();118 uint256 eventIdLen = bytes(p.eventId).length;119 uint256 categoryLen = bytes(p.category).length;120 if (eventIdLen == 0 || eventIdLen > MAX_EVENT_ID_LENGTH || categoryLen == 0 || categoryLen > MAX_CATEGORY_LENGTH) {121 revert BadEvent();122 }123 if (p.latE6 < -90_000_000 || p.latE6 > 90_000_000 || p.lngE6 < -180_000_000 || p.lngE6 > 180_000_000) {124 revert BadCoordinates();125 }126 }127 128 function _record(LaunchInput calldata p, address token, address curve) internal {129 _launches.push(130 Launch({131 token: token,132 curve: curve,133 creator: msg.sender,134 createdAt: uint64(block.timestamp),135 createdBlock: uint64(_blockNumber()),136 latE6: p.latE6,137 lngE6: p.lngE6,138 category: p.category,139 eventId: p.eventId140 })141 );142 _index[token] = _launches.length;143 emit Launched(token, msg.sender, _launches.length - 1, curve, p.eventId);144 }145 146 /// If the curve handed back unspent ETH (e.g. it filled up mid-buy), pass it on.147 function _refundLeftover() internal {148 uint256 leftover = address(this).balance;149 if (leftover > 0) {150 (bool ok,) = msg.sender.call{value: leftover}("");151 if (!ok) revert RefundFailed();152 }153 }154 155 function _tokenParams(LaunchInput calldata p) internal returns (IPonsFactory.TokenParams memory t) {156 t.name = string.concat(NAME_PREFIX, p.name);157 t.symbol = p.symbol;158 t.logo = p.logo;159 t.description = p.description;160 t.socials.twitter = p.twitter;161 t.socials.telegram = p.telegram;162 // The launch index is known before the token address: /l/<index> redirects to the token page.163 t.socials.website = string.concat(WEBSITE, "/l/", Strings.toString(_launches.length));164 t.creatorFeeRecipient = msg.sender;165 t.creatorTaxBps = p.creatorTaxBps;166 t.buybackEnabled = p.buybackEnabled;167 t.expectedEconomics = p.expectedEconomics;168 // pons namespaces salts by launcher, so every Sightline launch shares one namespace.169 // Deriving the salt from the creator and their nonce keeps launches from colliding170 // and stops anyone from squatting another creator's next address.171 t.salt = keccak256(abi.encode(msg.sender, launchNonce[msg.sender]++));172 }173 174 /// The chain's own block number: ArbSys on Arbitrum chains, block.number elsewhere.175 function _blockNumber() internal view returns (uint256) {176 if (address(100).code.length == 0) return block.number;177 try IArbSys(address(100)).arbBlockNumber() returns (uint256 n) {178 return n;179 } catch {180 return block.number;181 }182 }183 184 // ----------------------------------------------------------------- views185 186 function launchCount() external view returns (uint256) {187 return _launches.length;188 }189 190 function getLaunch(address token) external view returns (Launch memory) {191 uint256 i = _index[token];192 if (i == 0) revert UnknownToken();193 return _launches[i - 1];194 }195 196 /// @notice Page through launches, oldest first. Returns fewer than `limit` at the end.197 function getLaunches(uint256 offset, uint256 limit) external view returns (Launch[] memory page) {198 uint256 total = _launches.length;199 if (offset >= total) return new Launch[](0);200 uint256 end = offset + limit > total ? total : offset + limit;201 page = new Launch[](end - offset);202 for (uint256 i = offset; i < end; i++) {203 page[i - offset] = _launches[i];204 }205 }206 207 /// Receives refunds from pons curves during a launch.208 receive() external payable {}209}