← Contracts

Contracts · MIT license

SightlineLauncher.v2.sol

Launcher v2 exactly as deployed: STLN names and per-token Sightline links.

Download .sol
1// SPDX-License-Identifier: MIT
2pragma 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 SightlineLauncher
14/// @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 fee
22/// 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 here
28 string symbol;
29 string logo; // image URI (https:// or ipfs://), optional
30 string description;
31 string twitter; // optional creator socials
32 string telegram;
33 uint16 creatorTaxBps; // extra creator fee on trades, capped by pons
34 bool buybackEnabled;
35 uint256 launchConfigId;
36 bytes32 expectedEconomics; // pons.previewLaunchEconomics(launchConfigId, ETH); guards against config changes
37 // 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 eventId
72 );
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, which
87 /// pons exempts from its anti-sniping tax).
88 function launch(LaunchInput calldata p, uint256 minTokensOut)
89 external
90 payable
91 nonReentrant
92 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.eventId
124 })
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 colliding
152 // 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 // ----------------------------------------------------------------- views
167
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}