← Contracts

Tests · MIT license

SightlineLauncher.t.sol

Launcher tests: signature, fees to the creator, first buy, refunds, validation, salts.

Download .sol
1// SPDX-License-Identifier: MIT
2pragma solidity ^0.8.28;
3
4import {Test} from "forge-std/Test.sol";
5import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
6import {MockPonsFactory} from "./mocks/MockPons.sol";
7import {IPonsFactory} from "../src/pons/IPons.sol";
8import {SightlineLauncher} from "../src/pons/SightlineLauncher.sol";
9
10contract RejectsEth {
11 receive() external payable {
12 revert("no");
13 }
14}
15
16contract SightlineLauncherTest is Test {
17 MockPonsFactory pons;
18 SightlineLauncher launcher;
19 address alice = makeAddr("alice");
20 address bob = makeAddr("bob");
21
22 address[] NONE;
23
24 function setUp() public {
25 pons = new MockPonsFactory();
26 launcher = new SightlineLauncher(IPonsFactory(address(pons)));
27 vm.deal(alice, 10 ether);
28 vm.deal(bob, 10 ether);
29 }
30
31 function _input() internal pure returns (SightlineLauncher.LaunchInput memory p) {
32 p.name = "United States Grand Prix";
33 p.symbol = "USGP26";
34 p.description = "Round 18, Circuit of the Americas.";
35 p.twitter = "https://x.com/someone";
36 p.creatorTaxBps = 200;
37 p.expectedEconomics = keccak256("econ");
38 p.category = "upcoming";
39 p.eventId = "f1:2026-18";
40 p.latE6 = 30_132_800;
41 p.lngE6 = -97_641_100;
42 }
43
44 function test_launch_signsAndForwardsParams() public {
45 vm.prank(alice);
46 (address token, address curve,) = launcher.launch{value: 0.0005 ether}(_input(), NONE, 0);
47
48 assertEq(pons.lastLauncher(), address(launcher), "Sightline is the launcher");
49 (
50 string memory name,
51 string memory symbol,,
52 string memory description,
53 IPonsFactory.Socials memory socials,
54 address feeRecipient,
55 uint16 tax,
56 bool buyback,,
57 ) = pons.last();
58 assertEq(name, "STLN - United States Grand Prix", "name signature");
59 assertEq(symbol, "USGP26");
60 assertEq(description, "Round 18, Circuit of the Americas.");
61 assertEq(socials.website, "https://trysightline.io/l/0", "website is the token's Sightline page");
62 assertEq(socials.twitter, "https://x.com/someone");
63 assertEq(feeRecipient, alice, "creator earns the fees");
64 assertEq(tax, 200);
65 assertFalse(buyback);
66
67 SightlineLauncher.Launch memory l = launcher.getLaunch(token);
68 assertEq(l.curve, curve);
69 assertEq(l.creator, alice);
70 assertEq(l.eventId, "f1:2026-18");
71 assertEq(l.category, "upcoming");
72 assertEq(l.latE6, 30_132_800);
73 assertEq(l.createdBlock, block.number);
74 assertEq(launcher.launchCount(), 1);
75 }
76
77 function test_launch_websiteCannotBeOverridden() public {
78 // There's no website field in the input at all; check the stored value regardless of other socials.
79 SightlineLauncher.LaunchInput memory p = _input();
80 p.telegram = "https://t.me/x";
81 vm.prank(alice);
82 launcher.launch{value: 0.0005 ether}(p, NONE, 0);
83 (,,,, IPonsFactory.Socials memory socials,,,,,) = pons.last();
84 assertEq(socials.website, "https://trysightline.io/l/0");
85 assertEq(socials.telegram, "https://t.me/x");
86 }
87
88 function test_launch_websiteFollowsLaunchIndex() public {
89 vm.startPrank(alice);
90 launcher.launch{value: 0.0005 ether}(_input(), NONE, 0);
91 launcher.launch{value: 0.0005 ether}(_input(), NONE, 0);
92 launcher.launch{value: 0.0005 ether}(_input(), NONE, 0);
93 vm.stopPrank();
94 (,,,, IPonsFactory.Socials memory socials,,,,,) = pons.last();
95 assertEq(socials.website, "https://trysightline.io/l/2");
96 }
97
98 function test_launch_validatesName() public {
99 SightlineLauncher.LaunchInput memory p = _input();
100 p.name = "";
101 vm.prank(alice);
102 vm.expectRevert(SightlineLauncher.BadName.selector);
103 launcher.launch{value: 0.0005 ether}(p, NONE, 0);
104
105 p.name = string(new bytes(49));
106 vm.prank(alice);
107 vm.expectRevert(SightlineLauncher.BadName.selector);
108 launcher.launch{value: 0.0005 ether}(p, NONE, 0);
109 }
110
111 function test_launch_recordsArbitrumBlockWhenAvailable() public {
112 // Off Arbitrum there's no ArbSys: block.number. With ArbSys: its L2 block number.
113 vm.roll(1234);
114 vm.prank(alice);
115 (address token,,) = launcher.launch{value: 0.0005 ether}(_input(), NONE, 0);
116 assertEq(launcher.getLaunch(token).createdBlock, 1234);
117
118 vm.etch(address(100), hex"fe");
119 vm.mockCall(address(100), abi.encodeWithSignature("arbBlockNumber()"), abi.encode(uint256(88_000_000)));
120 vm.prank(alice);
121 (address token2,,) = launcher.launch{value: 0.0005 ether}(_input(), NONE, 0);
122 assertEq(launcher.getLaunch(token2).createdBlock, 88_000_000);
123 }
124
125 function test_launch_forwardsSnipeExemptions() public {
126 address[] memory ex = new address[](2);
127 ex[0] = bob;
128 ex[1] = makeAddr("team");
129 vm.prank(alice);
130 launcher.launch{value: 0.0005 ether}(_input(), ex, 0);
131 address[] memory got = pons.lastExemptions();
132 assertEq(got.length, 2);
133 assertEq(got[0], bob);
134 }
135
136 function test_launch_capsSnipeExemptions() public {
137 address[] memory ex = new address[](31);
138 for (uint256 i = 0; i < 31; i++) ex[i] = address(uint160(i + 1));
139 vm.prank(alice);
140 vm.expectRevert(SightlineLauncher.TooManyExemptions.selector);
141 launcher.launch{value: 0.0005 ether}(_input(), ex, 0);
142 }
143
144 function test_launch_devBuyGoesToCreator() public {
145 vm.prank(alice);
146 (address token,, uint256 out) = launcher.launch{value: 0.1005 ether}(_input(), NONE, 1);
147 assertGt(out, 0);
148 assertEq(ERC20(token).balanceOf(alice), out);
149 assertEq(ERC20(token).balanceOf(address(launcher)), 0);
150 assertEq(address(launcher).balance, 0);
151 assertEq(alice.balance, 10 ether - 0.1005 ether);
152 }
153
154 function test_launch_devBuySlippage() public {
155 vm.prank(alice);
156 vm.expectRevert("slippage");
157 launcher.launch{value: 0.1005 ether}(_input(), NONE, type(uint256).max);
158 }
159
160 function test_launch_refundsUnspentEth() public {
161 // The curve fills up mid-buy and hands back half; the creator gets it, the launcher keeps nothing.
162 pons.setNextRefundBps(5_000);
163 vm.prank(alice);
164 launcher.launch{value: 1.0005 ether}(_input(), NONE, 0);
165 assertEq(address(launcher).balance, 0);
166 assertEq(alice.balance, 10 ether - 0.5005 ether);
167 }
168
169 function test_launch_requiresFee() public {
170 vm.prank(alice);
171 vm.expectRevert(SightlineLauncher.InsufficientLaunchFee.selector);
172 launcher.launch{value: 0.0004 ether}(_input(), NONE, 0);
173 }
174
175 function test_launch_validatesEvent() public {
176 SightlineLauncher.LaunchInput memory p = _input();
177 p.eventId = "";
178 vm.prank(alice);
179 vm.expectRevert(SightlineLauncher.BadEvent.selector);
180 launcher.launch{value: 0.0005 ether}(p, NONE, 0);
181
182 p = _input();
183 p.category = "";
184 vm.prank(alice);
185 vm.expectRevert(SightlineLauncher.BadEvent.selector);
186 launcher.launch{value: 0.0005 ether}(p, NONE, 0);
187
188 p = _input();
189 p.latE6 = 90_000_001;
190 vm.prank(alice);
191 vm.expectRevert(SightlineLauncher.BadCoordinates.selector);
192 launcher.launch{value: 0.0005 ether}(p, NONE, 0);
193 }
194
195 function test_salts_neverCollide() public {
196 // Same creator twice, and two creators with identical inputs: all distinct salts.
197 vm.prank(alice);
198 launcher.launch{value: 0.0005 ether}(_input(), NONE, 0);
199 vm.prank(alice);
200 launcher.launch{value: 0.0005 ether}(_input(), NONE, 0);
201 vm.prank(bob);
202 launcher.launch{value: 0.0005 ether}(_input(), NONE, 0);
203 assertEq(launcher.launchCount(), 3);
204 assertEq(launcher.launchNonce(alice), 2);
205 assertEq(launcher.launchNonce(bob), 1);
206 }
207
208 function test_refundFailure_reverts() public {
209 RejectsEth r = new RejectsEth();
210 vm.deal(address(r), 1 ether);
211 // Pre-fund the launcher so there's "leftover" to return; a creator that can't take it reverts.
212 vm.deal(address(launcher), 1 wei);
213 vm.prank(address(r));
214 vm.expectRevert(SightlineLauncher.RefundFailed.selector);
215 launcher.launch{value: 0.0005 ether}(_input(), NONE, 0);
216 }
217
218 function test_getLaunches_pages() public {
219 for (uint256 i = 0; i < 3; i++) {
220 vm.prank(alice);
221 launcher.launch{value: 0.0005 ether}(_input(), NONE, 0);
222 }
223 assertEq(launcher.getLaunches(0, 2).length, 2);
224 assertEq(launcher.getLaunches(2, 5).length, 1);
225 assertEq(launcher.getLaunches(3, 5).length, 0);
226 vm.expectRevert(SightlineLauncher.UnknownToken.selector);
227 launcher.getLaunch(address(0xBEEF));
228 }
229}