Tests · MIT license
SightlineLauncher.t.sol
Launcher tests: signature, fees to the creator, first buy, refunds, validation, salts.
1// SPDX-License-Identifier: MIT2pragma 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}