Back to Contract Registry
LaunchVault
Token sale contract (use contribute() only).
0xE49bB5d26bad732534e0122EbceaBa3486cC94c5
LOCKITLaunchVault Base Mainnet Verified on Basescan
Source Viewer
1 // SPDX-License-Identifier: MIT
2 pragma solidity ^0.8.20;
3
4 /**
5 * @title LOCKITLaunchVault
6 * @author LockItIn DAO
7 * @notice Trust-minimized token sale contract for LOCKIT token
8 * @dev Solidity 0.8.20, optimizer ON, runs = 777
9 *
10 * VERSION: 2.2 (USDC Distribution Alignment)
11 * CHANGES FROM v2.1:
12 * - FIXED: Removed incorrect DAO_USDC_ALLOCATION ($125k that shouldn't exist)
13 * - FIXED: Renamed legalWallet → complianceWallet with $250k allocation
14 * - FIXED: USDC distribution now matches Services Agreement Section 6.2:
15 * * $50,000 → LP (burned)
16 * * $250,000 → Compliance Budget
17 * * $125,000 → Development Services
18 * * $75,000 → Infrastructure Budget
19 * - Note: daoTreasury still used for TOKEN distribution and transfer fallbacks
20 *
21 * VERSION: 2.1 (LP Allocation Update)
22 * CHANGES FROM v2.0:
23 * - Updated: LP allocation increased from 4% to 5% (40M → 50M tokens)
24 * - Updated: DAO vesting reduced from 36% to 35% (360M → 350M tokens)
25 * - Note: LP USDC remains $50k, resulting in cleaner $0.001/token LP price
26 *
27 * VERSION: 2.0 (Final)
28 * CHANGES FROM v1:
29 * - Fixed: Contributions now permanently blocked once goal reached (monotonic lifecycle)
30 * - Fixed: createPair() race condition handled via re-check after failed create
31 * - Fixed: _isSaleReady() semantics cleaner for post-finalization state
32 * - Fixed: Distinct PairTokenMismatch error instead of reusing LPNotBurned
33 *
34 * Architecture:
35 * - Pre-mint 1B LOCKIT tokens to this contract before sale opens
36 * - Contributors send USDC, receive proportional LOCKIT after TGE
37 * - When goal is met, anyone can call finalize()
38 * - finalize() creates permanent Uniswap V2 liquidity + distributes to all fixed recipients
39 * - Contributors can claim() their tokens after finalization (pull-based)
40 * - distribute() can also push tokens to contributors in batches (anyone can call)
41 * - After deadline, sale closes; contributors can refund if goal not met
42 *
43 * Trust Model:
44 * - All allocation percentages are immutable constants
45 * - All recipient addresses are immutable (set in constructor)
46 * - No admin functions that can redirect funds
47 * - LP tokens are burned (sent to 0xdead) making liquidity permanent
48 * - Only escape hatch: refund() if goal not met by deadline
49 *
50 * CRITICAL OPERATIONAL INVARIANT:
51 * - The ENTIRE 1B LOCKIT supply must be transferred to this vault before ANY
52 * tokens are distributed elsewhere
53 * - If ANY LOCKIT exists outside this vault before finalization, an attacker
54 * could initialize the Uniswap pair and grief the automated LP creation
55 * - The sale cannot start until the full 1B is deposited (enforced in code)
56 * - Deploy → immediately transfer all tokens → then announce sale
57 *
58 * LP Creation Strategy (Griefing-Resistant + Atomic):
59 * - For uninitialized pairs (totalSupply == 0), we bypass the router entirely
60 * - We transfer tokens directly to the pair and call mint()
61 * - CRITICAL: All LP operations (transfer + mint + burn) are wrapped in an
62 * external self-call so they revert atomically if any step fails
63 * - This prevents the "tokens stranded in pair" brick scenario
64 * - This makes sync() griefing attacks economically irrelevant:
65 * * Attacker's dust gets absorbed into our first mint
66 * * Our 50M LOCKIT + $50k USDC dominates any dust amounts
67 * * First mint uses sqrt(amount0 * amount1) where amounts = balance - reserve
68 * - For already-initialized pairs, we fail safely into refund path
69 * - createPair() front-running is handled by re-checking getPair() after failure
70 *
71 * Failed Sale Behavior:
72 * - If deadline passes and goal is not met, contributors can refund()
73 * - The 1B LOCKIT tokens remain locked in this contract forever (effective burn)
74 * - This is intentional: no admin can recover tokens from a failed sale
75 * - A new sale would require minting a new token
76 *
77 * Emergency Refund (Finalize Stuck):
78 * - If goal IS met but finalize() keeps failing (LP creation blocked somehow)
79 * - After goalReachedAt + 180 days, emergency refunds become available
80 * - This is INDEPENDENT of the deadline - emergency refunds use goalReachedAt timing
81 * - Contributors can get their USDC back even though goal was met
82 * - LOCKIT tokens remain locked forever (effective burn)
83 * - IMPORTANT: Emergency refunds are BLOCKED if manual override was executed
84 * - This prevents partial refunds (vault would lack full USDC after override)
85 *
86 * Manual Override System (LP Creation Fails):
87 * - If finalize() fails 5 times (100+ blocks between each attempt)
88 * - manualOverrideEnabled becomes true
89 * - ONLY lpOpsWallet can call executeManualOverride() (prevents griefing)
90 * - executeManualOverride() sends LP allocation (50M LOCKIT + $50k) to lpOpsWallet
91 * - lpOpsWallet manually creates LP on Uniswap (fast, no governance delay)
92 * - completeFinalization() verifies LP was burned (minimum threshold check)
93 * - This allows launch to proceed even if automated LP creation fails
94 * - Once override executed, emergency refunds are blocked (must complete finalization)
95 * - Override requires full balances intact (prevents post-refund execution)
96 * - ESCAPE HATCH: If override executed but not completed after 90 days,
97 * contributors can claim prorated refunds via overrideEscapeRefund()
98 * - IMPORTANT: Once any escape refund is claimed, completeFinalization() is permanently
99 * blocked to prevent mixed states (some refunded, some claiming tokens)
100 *
101 * Blacklist-Safe USDC Transfers:
102 * - All USDC payouts to operational wallets use _safeUSDCTransferOrFallback()
103 * - If a recipient is USDC-blacklisted, funds go to daoTreasury instead
104 * - Emits PaymentFailed event for transparency
105 * - NOTE: If daoTreasury itself is blacklisted, finalization will revert
106 * - This is acceptable: if the DAO is sanctioned, we don't want to proceed
107 *
108 * Pre-Sale Safety:
109 * - Contributions are rejected until TOTAL_SUPPLY tokens are deposited
110 * - Prevents "bricked sale" if deployment is botched
111 * - Use isSaleReady() to verify before announcing sale
112 *
113 * Deadline Behavior:
114 * - Deadline is set at deployment time (block.timestamp + saleDurationDays)
115 * - IMPORTANT: Deposit 1B tokens immediately after deployment
116 * - The sale duration counts from deployment, not from token deposit time
117 *
118 * Extra USDC Handling:
119 * - USDC sent directly to the contract (not via contribute()) is NOT tracked
120 * - Such USDC becomes permanently locked (by design - maintains trust model)
121 * - Only use contribute() or contributeWithPermit() to participate
122 *
123 * Near-Goal Contribution Handling:
124 * - Minimum contribution is $10, but if remaining capacity is less than $10,
125 * contributors can contribute exactly the remaining amount to complete the sale
126 * - This prevents the sale from getting stuck near the goal
127 *
128 * Monotonic Sale Lifecycle (v2 fix):
129 * - Once goal is reached (goalReachedAt != 0), no new contributions are ever accepted
130 * - This prevents "re-opening" the sale after refunds reduce totalRaised
131 * - Sale states: Open → Goal Reached → Finalize/Override/Refund (one-way only)
132 *
133 * Token Allocation (1,000,000,000 LOCKIT):
134 * - 50.0% (500M) → Token sale buyers (claimed after finalize)
135 * - 5.0% (50M) → Uniswap V2 LP (burned, permanent liquidity)
136 * - 1.0% (10M) → Founder immediate
137 * - 4.0% (40M) → Founder vesting (7y7m7d linear)
138 * - 5.0% (50M) → DAO treasury immediate
139 * - 35.0% (350M) → DAO vesting (11y11m11d linear)
140 *
141 * USDC Allocation ($500,000 raise) - Per Services Agreement Section 6.2:
142 * - $50,000 → Uniswap V2 LP (paired with 50M tokens)
143 * - $250,000 → Compliance Budget (managed by Labs)
144 * - $125,000 → Development Services (Labs compensation)
145 * - $75,000 → Infrastructure Budget (managed by Labs)
146 */
147
148 import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
149 import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
150 import {IERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol";
151 import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
152 import {Math} from "@openzeppelin/contracts/utils/math/Math.sol";
153
154 interface IUniswapV2Factory {
155 function getPair(address tokenA, address tokenB) external view returns (address pair);
156 function createPair(address tokenA, address tokenB) external returns (address pair);
157 }
158
159 interface IUniswapV2Pair {
160 function totalSupply() external view returns (uint256);
161 function balanceOf(address owner) external view returns (uint256);
162 function mint(address to) external returns (uint256 liquidity);
163 function token0() external view returns (address);
164 function token1() external view returns (address);
165 }
166
167 contract LOCKITLaunchVault is ReentrancyGuard {
168 using SafeERC20 for IERC20;
169
170 // ═══════════════════════════════════════════════════════════════════════════
171 // ERRORS
172 // ═══════════════════════════════════════════════════════════════════════════
173
174 error SaleNotOpen();
175 error SaleEnded();
176 error GoalAlreadyMet();
177 error BelowMinContribution();
178 error ExceedsMaxContribution();
179 error ExceedsGoal();
180 error GoalNotMet();
181 error NotRefundable();
182 error AlreadyFinalized();
183 error NotFinalized();
184 error NoContribution();
185 error AlreadyClaimed();
186 error DistributionComplete();
187 error InvalidAddress();
188 error InsufficientTokenBalance();
189 error InsufficientUSDCBalance();
190 error PermitDeadlineExpired();
191 error CannotRescueLOCKIT();
192 error CannotRescueUSDC();
193 error NoTokensToRescue();
194 error TooSoonToRetry();
195 error ManualOverrideNotEnabled();
196 error ManualOverrideAlreadyExecuted();
197 error ManualOverrideNotExecuted();
198 error UseCompleteFinalization();
199 error EmergencyWindowOpen();
200 error OverrideEscapeNotAvailable();
201 error OverrideEscapeActivated();
202 error TokenAddressesMustDiffer();
203 error ZeroDurationNotAllowed();
204 error LPNotBurned();
205 error InsufficientLPBurned();
206 error PairTokenMismatch();
207 error OnlySelf();
208 error ZeroLiquidityMinted();
209 error OnlyLpOpsWallet();
210
211 // ═══════════════════════════════════════════════════════════════════════════
212 // EVENTS
213 // ═══════════════════════════════════════════════════════════════════════════
214
215 event ContributionReceived(address indexed contributor, uint256 amount, uint256 totalContributed);
216 event SaleFinalized(uint256 totalRaised, uint256 contributorCount, uint256 lpTokensBurned);
217 event TokensClaimed(address indexed contributor, uint256 amount);
218 event TokensDistributed(address indexed contributor, uint256 amount);
219 event BatchDistributed(uint256 startIndex, uint256 endIndex, uint256 totalDistributed);
220 event RefundClaimed(address indexed contributor, uint256 amount);
221 event OverrideEscapeRefundClaimed(address indexed contributor, uint256 amount);
222 event LiquidityCreated(address indexed pair, uint256 liquidity);
223 event LPTokensBurned(address indexed pair, uint256 amount);
224 event TokensRescued(address indexed token, uint256 amount, address indexed recipient);
225 event PaymentFailed(address indexed recipient, uint256 amount, address indexed fallbackRecipient);
226 event GoalReached(uint256 timestamp, uint256 totalContributors);
227
228 // Manual override events
229 event FinalizeAttemptFailed(uint256 attemptNumber, uint256 blockNumber, string reason);
230 event ManualOverrideEnabled(uint256 blockNumber, uint256 totalAttempts);
231 event ManualOverrideExecuted(uint256 tokenAmount, uint256 usdcAmount, uint256 timestamp);
232 event FinalizationCompleted(uint256 totalRaised, uint256 contributorCount);
233
234 // ═══════════════════════════════════════════════════════════════════════════
235 // CONSTANTS - Token Allocation (immutable percentages)
236 // ═══════════════════════════════════════════════════════════════════════════
237
238 uint256 public constant TOTAL_SUPPLY = 1_000_000_000e18; // 1 billion tokens
239
240 uint256 public constant SALE_ALLOCATION = 500_000_000e18; // 50% - Token sale
241 uint256 public constant LP_TOKEN_ALLOCATION = 50_000_000e18; // 5% - Uniswap LP (burned)
242 uint256 public constant FOUNDER_IMMEDIATE = 10_000_000e18; // 1% - Founder at TGE
243 uint256 public constant FOUNDER_VESTING = 40_000_000e18; // 4% - Founder vesting
244 uint256 public constant DAO_IMMEDIATE = 50_000_000e18; // 5% - DAO treasury at TGE
245 uint256 public constant DAO_VESTING = 350_000_000e18; // 35% - DAO vesting
246
247 // ═══════════════════════════════════════════════════════════════════════════
248 // CONSTANTS - Sale Parameters
249 // ═══════════════════════════════════════════════════════════════════════════
250
251 uint256 public constant GOAL = 500_000e6; // $500,000 USDC (6 decimals)
252 uint256 public constant MIN_CONTRIBUTION = 10e6; // $10 USDC
253 uint256 public constant MAX_CONTRIBUTION = 25_000e6; // $25,000 USDC
254
255 // USDC allocation constants - Per Services Agreement Section 6.2
256 // v2.2 FIX: Removed DAO_USDC_ALLOCATION, renamed LEGAL to COMPLIANCE with correct amount
257 uint256 public constant LP_USDC_ALLOCATION = 50_000e6; // $50,000 for LP
258 uint256 public constant COMPLIANCE_USDC_ALLOCATION = 250_000e6; // $250,000 to Compliance Budget
259 uint256 public constant DEV_USDC_ALLOCATION = 125_000e6; // $125,000 to Development Services
260 uint256 public constant INFRA_USDC_ALLOCATION = 75_000e6; // $75,000 to Infrastructure Budget
261
262 // Token price: $0.001 per token
263 // 500,000,000 tokens / $500,000 = 1000 tokens per dollar
264 // Or: 1 USDC (1e6) buys 1000e18 tokens
265 uint256 public constant TOKENS_PER_USDC = 1000e18; // 1000 tokens per $1 (in wei per USDC unit)
266
267 address public constant BURN_ADDRESS = 0x000000000000000000000000000000000000dEaD;
268
269 // Uniswap V2 minimum liquidity (burned to address(0) on first mint)
270 uint256 public constant UNISWAP_MINIMUM_LIQUIDITY = 1000;
271
272 // Manual override parameters (if LP creation repeatedly fails)
273 uint256 public constant MAX_FAILED_ATTEMPTS = 5;
274 uint256 public constant MIN_BLOCKS_BETWEEN_ATTEMPTS = 100;
275
276 // Emergency refund delay (absolute last resort - 180 days after goal reached)
277 uint256 public constant EMERGENCY_REFUND_DELAY = 180 days;
278
279 // Override escape delay (if override executed but not completed - 90 days)
280 uint256 public constant OVERRIDE_ESCAPE_DELAY = 90 days;
281
282 // ═══════════════════════════════════════════════════════════════════════════
283 // IMMUTABLE STATE (set in constructor, never changes)
284 // ═══════════════════════════════════════════════════════════════════════════
285
286 IERC20 public immutable lockitToken;
287 IERC20 public immutable usdc;
288 address public immutable uniswapFactory;
289
290 uint256 public immutable deadline;
291
292 // Recipient addresses (all verified before contributing)
293 address public immutable founderWallet;
294 address public immutable founderVestingContract;
295 address public immutable daoTreasury; // Receives TOKENS + serves as USDC fallback for failed transfers
296 address public immutable daoVestingContract;
297 address public immutable complianceWallet; // $250k USDC - Compliance Budget (was "legalWallet" in v2.1)
298 address public immutable devWallet; // $125k USDC - Development Services
299 address public immutable infraWallet; // $75k USDC - Infrastructure Budget
300 address public immutable lpOpsWallet; // Receives LP allocation if manual override needed
301
302 // ═══════════════════════════════════════════════════════════════════════════
303 // MUTABLE STATE
304 // ═══════════════════════════════════════════════════════════════════════════
305
306 // Contribution tracking
307 mapping(address => uint256) public contributions;
308 address[] public contributors;
309 mapping(address => bool) public hasContributed;
310
311 uint256 public totalRaised;
312 uint256 public goalReachedAt; // Timestamp when goal was first reached (0 if not yet)
313
314 // Sale state
315 bool public finalized;
316
317 // Distribution tracking (for batched token push and pull-based claims)
318 uint256 public distributionIndex;
319 mapping(address => bool) public tokensReceived;
320
321 // Manual override tracking (if LP creation fails)
322 uint256 public failedAttempts;
323 uint256 public lastAttemptBlock;
324 bool public manualOverrideEnabled;
325 bool public manualOverrideExecuted;
326 uint256 public manualOverrideExecutedAt; // Timestamp for escape hatch timing
327
328 // Override escape state - once activated, completeFinalization() is permanently blocked
329 bool public overrideEscapeActivated;
330
331 // LP pair address (set during finalization for verification)
332 address public lpPair;
333
334 // ═══════════════════════════════════════════════════════════════════════════
335 // CONSTRUCTOR
336 // ═══════════════════════════════════════════════════════════════════════════
337
338 /**
339 * @notice Deploy the LaunchVault
340 * @dev All recipient addresses must be non-zero and verified before deployment.
341 * IMPORTANT: Transfer all 1B LOCKIT tokens immediately after deployment.
342 * The deadline starts at deployment time, not at token deposit time.
343 *
344 * v2.2 CHANGE: Renamed _legalWallet → _complianceWallet
345 * v2.2 CHANGE: daoTreasury no longer receives USDC (only tokens + fallback)
346 *
347 * @param _lockitToken LOCKIT token address (must hold 1B tokens)
348 * @param _usdc USDC token address
349 * @param _uniswapFactory Uniswap V2 Factory address (Base: 0x8909Dc15e40173Ff4699343b6eB8132c65e18eC6)
350 * @param _saleDurationDays Number of days the sale runs (must be > 0)
351 * @param _founderWallet Founder EOA for 1% immediate tokens
352 * @param _founderVesting Founder vesting contract for 4% tokens
353 * @param _daoTreasury DAO treasury for 5% tokens (also USDC fallback for failed transfers)
354 * @param _daoVesting DAO vesting contract for 35% tokens
355 * @param _complianceWallet Compliance Budget wallet for $250k USDC
356 * @param _devWallet Development wallet for $125k USDC
357 * @param _infraWallet Infrastructure wallet for $75k USDC
358 * @param _lpOpsWallet LP operations wallet for manual override (ONLY address that can trigger override)
359 */
360 constructor(
361 address _lockitToken,
362 address _usdc,
363 address _uniswapFactory,
364 uint256 _saleDurationDays,
365 address _founderWallet,
366 address _founderVesting,
367 address _daoTreasury,
368 address _daoVesting,
369 address _complianceWallet,
370 address _devWallet,
371 address _infraWallet,
372 address _lpOpsWallet
373 ) {
374 // Validate all addresses
375 if (_lockitToken == address(0)) revert InvalidAddress();
376 if (_usdc == address(0)) revert InvalidAddress();
377 if (_uniswapFactory == address(0)) revert InvalidAddress();
378 if (_founderWallet == address(0)) revert InvalidAddress();
379 if (_founderVesting == address(0)) revert InvalidAddress();
380 if (_daoTreasury == address(0)) revert InvalidAddress();
381 if (_daoVesting == address(0)) revert InvalidAddress();
382 if (_complianceWallet == address(0)) revert InvalidAddress();
383 if (_devWallet == address(0)) revert InvalidAddress();
384 if (_infraWallet == address(0)) revert InvalidAddress();
385 if (_lpOpsWallet == address(0)) revert InvalidAddress();
386
387 // Validate token addresses are different
388 if (_lockitToken == _usdc) revert TokenAddressesMustDiffer();
389
390 // Validate sale duration
391 if (_saleDurationDays == 0) revert ZeroDurationNotAllowed();
392
393 lockitToken = IERC20(_lockitToken);
394 usdc = IERC20(_usdc);
395 uniswapFactory = _uniswapFactory;
396
397 deadline = block.timestamp + (_saleDurationDays * 1 days);
398
399 founderWallet = _founderWallet;
400 founderVestingContract = _founderVesting;
401 daoTreasury = _daoTreasury;
402 daoVestingContract = _daoVesting;
403 complianceWallet = _complianceWallet;
404 devWallet = _devWallet;
405 infraWallet = _infraWallet;
406 lpOpsWallet = _lpOpsWallet;
407
408 // Sanity check: token allocations must equal total supply
409 assert(
410 SALE_ALLOCATION +
411 LP_TOKEN_ALLOCATION +
412 FOUNDER_IMMEDIATE +
413 FOUNDER_VESTING +
414 DAO_IMMEDIATE +
415 DAO_VESTING == TOTAL_SUPPLY
416 );
417
418 // Sanity check: USDC allocations must equal goal
419 // v2.2 FIX: Removed DAO_USDC_ALLOCATION from this sum
420 assert(
421 LP_USDC_ALLOCATION +
422 COMPLIANCE_USDC_ALLOCATION +
423 DEV_USDC_ALLOCATION +
424 INFRA_USDC_ALLOCATION == GOAL
425 );
426 }
427
428 // ═══════════════════════════════════════════════════════════════════════════
429 // INTERNAL HELPERS
430 // ═══════════════════════════════════════════════════════════════════════════
431
432 /**
433 * @dev Check if the sale is ready to accept contributions
434 * v2: Returns false after finalization (cleaner semantics for UIs)
435 */
436 function _isSaleReady() internal view returns (bool) {
437 // After finalization, the vault no longer holds TOTAL_SUPPLY
438 // Return true for "was ready, now complete" to avoid confusing UIs
439 if (finalized) return true;
440 return lockitToken.balanceOf(address(this)) >= TOTAL_SUPPLY;
441 }
442
443 /**
444 * @dev Check if the sale can currently accept contributions
445 * This is the actual "is open for business" check
446 */
447 function _canAcceptContributions() internal view returns (bool) {
448 if (finalized) return false;
449 if (block.timestamp >= deadline) return false;
450 if (goalReachedAt != 0) return false; // v2: Goal reached = permanently closed
451 if (manualOverrideExecuted) return false; // v2: Override path = permanently closed
452 if (lockitToken.balanceOf(address(this)) < TOTAL_SUPPLY) return false;
453 return true;
454 }
455
456 /**
457 * @dev Blacklist-safe USDC transfer: attempts transfer, falls back to daoTreasury on failure
458 * @param _recipient Intended recipient
459 * @param _amount Amount to transfer
460 * @return actualRecipient The address that actually received the funds
461 */
462 function _safeUSDCTransferOrFallback(address _recipient, uint256 _amount) internal returns (address actualRecipient) {
463 if (_recipient == address(0) || _recipient == daoTreasury) {
464 usdc.safeTransfer(daoTreasury, _amount);
465 return daoTreasury;
466 }
467
468 // Try low-level call to handle blacklist reverts
469 (bool success, bytes memory data) = address(usdc).call(
470 abi.encodeWithSelector(IERC20.transfer.selector, _recipient, _amount)
471 );
472
473 bool transferOk = _evaluateTransferResult(success, data);
474
475 if (transferOk) {
476 return _recipient;
477 } else {
478 // Fallback to treasury
479 usdc.safeTransfer(daoTreasury, _amount);
480 emit PaymentFailed(_recipient, _amount, daoTreasury);
481 return daoTreasury;
482 }
483 }
484
485 /**
486 * @dev Evaluate transfer result from low-level call
487 */
488 function _evaluateTransferResult(bool success, bytes memory data) internal pure returns (bool) {
489 if (!success) return false;
490 if (data.length == 0) return true; // No return data = success (some tokens)
491 if (data.length == 32) return abi.decode(data, (bool)); // Standard ERC20 return
492 return false;
493 }
494
495 // ═══════════════════════════════════════════════════════════════════════════
496 // CONTRIBUTION FUNCTIONS
497 // ═══════════════════════════════════════════════════════════════════════════
498
499 /**
500 * @notice Contribute USDC to the token sale
501 * @param amount Amount of USDC to contribute (6 decimals)
502 */
503 function contribute(uint256 amount) external nonReentrant {
504 _contribute(msg.sender, amount);
505 }
506
507 /**
508 * @notice Contribute with EIP-2612 permit (single transaction)
509 * @param amount Amount of USDC to contribute
510 * @param permitDeadline Permit signature deadline
511 * @param v Signature v
512 * @param r Signature r
513 * @param s Signature s
514 */
515 function contributeWithPermit(
516 uint256 amount,
517 uint256 permitDeadline,
518 uint8 v,
519 bytes32 r,
520 bytes32 s
521 ) external nonReentrant {
522 if (block.timestamp > permitDeadline) revert PermitDeadlineExpired();
523
524 IERC20Permit(address(usdc)).permit(
525 msg.sender,
526 address(this),
527 amount,
528 permitDeadline,
529 v,
530 r,
531 s
532 );
533
534 _contribute(msg.sender, amount);
535 }
536
537 /**
538 * @notice Internal contribution logic
539 * @dev v2 CHANGES:
540 * - Once goal has EVER been reached (goalReachedAt != 0), no new contributions
541 * - Once manual override is executed, no new contributions
542 * - This ensures monotonic sale lifecycle: Open → Goal Reached → Finalize/Refund
543 * - Prevents "re-opening" after refunds reduce totalRaised below GOAL
544 */
545 function _contribute(address contributor, uint256 amount) internal {
546 // Verify tokens are deposited (prevents bricked sale if deployment botched)
547 if (lockitToken.balanceOf(address(this)) < TOTAL_SUPPLY) revert SaleNotOpen();
548
549 // Check sale is open - block contributions after deadline
550 if (finalized) revert SaleEnded();
551 if (block.timestamp >= deadline) revert SaleEnded();
552
553 // v2 FIX: Once goal has EVER been reached, no new contributions
554 // This prevents the "sale re-opens after refunds" bug
555 if (goalReachedAt != 0) revert GoalAlreadyMet();
556
557 // v2 FIX: Block contributions if override path is active (defense in depth)
558 if (manualOverrideExecuted) revert SaleEnded();
559
560 uint256 remaining = GOAL - totalRaised;
561
562 // Allow contribution below MIN if it exactly fills remaining capacity
563 // This prevents the "brick near goal" scenario where remaining < MIN_CONTRIBUTION
564 if (amount < MIN_CONTRIBUTION && amount != remaining) revert BelowMinContribution();
565
566 uint256 newTotal = contributions[contributor] + amount;
567 if (newTotal > MAX_CONTRIBUTION) revert ExceedsMaxContribution();
568
569 // Don't exceed goal
570 if (amount > remaining) revert ExceedsGoal();
571
572 // Transfer USDC from contributor
573 usdc.safeTransferFrom(contributor, address(this), amount);
574
575 // Track contribution
576 if (!hasContributed[contributor]) {
577 hasContributed[contributor] = true;
578 contributors.push(contributor);
579 }
580 contributions[contributor] = newTotal;
581 totalRaised += amount;
582
583 emit ContributionReceived(contributor, amount, newTotal);
584
585 // Record when goal was first reached (for emergency refund timing)
586 // This is the ONLY place goalReachedAt gets set, making it monotonic
587 if (totalRaised >= GOAL && goalReachedAt == 0) {
588 goalReachedAt = block.timestamp;
589 emit GoalReached(block.timestamp, contributors.length);
590 }
591 }
592
593 // ═══════════════════════════════════════════════════════════════════════════
594 // FINALIZATION
595 // ═══════════════════════════════════════════════════════════════════════════
596
597 /**
598 * @notice Finalize the sale when goal is met
599 * @dev Anyone can call this once goal is reached
600 * If LP creation fails, records attempt and may enable manual override
601 * After 5 failed attempts (100 blocks apart), manual override becomes available
602 * Uses blacklist-safe transfers for all USDC payouts
603 *
604 * v2.2 CHANGE: Removed USDC transfer to daoTreasury (only tokens now)
605 */
606 function finalize() external nonReentrant {
607 if (finalized) revert AlreadyFinalized();
608 if (manualOverrideExecuted) revert UseCompleteFinalization();
609 if (totalRaised < GOAL) revert GoalNotMet();
610
611 // Check attempt spacing (100 blocks minimum between attempts)
612 if (failedAttempts > 0 && block.number < lastAttemptBlock + MIN_BLOCKS_BETWEEN_ATTEMPTS) {
613 revert TooSoonToRetry();
614 }
615
616 // Verify we have the tokens
617 uint256 tokenBalance = lockitToken.balanceOf(address(this));
618 if (tokenBalance < TOTAL_SUPPLY) revert InsufficientTokenBalance();
619
620 // Verify we have the USDC
621 uint256 usdcBalance = usdc.balanceOf(address(this));
622 if (usdcBalance < GOAL) revert InsufficientUSDCBalance();
623
624 // ─────────────────────────────────────────────────────────────────────
625 // STEP 1: Try to create permanent Uniswap V2 liquidity
626 // ─────────────────────────────────────────────────────────────────────
627
628 (bool lpSuccess, uint256 lpTokensBurned, string memory failReason) = _tryCreateAndBurnLP();
629
630 if (!lpSuccess) {
631 // Record failed attempt
632 lastAttemptBlock = block.number;
633 failedAttempts++;
634
635 emit FinalizeAttemptFailed(failedAttempts, block.number, failReason);
636
637 // Check if manual override should be enabled
638 if (failedAttempts >= MAX_FAILED_ATTEMPTS) {
639 manualOverrideEnabled = true;
640 emit ManualOverrideEnabled(block.number, failedAttempts);
641 }
642
643 return; // Exit without reverting - attempt recorded
644 }
645
646 // LP succeeded - complete finalization
647 finalized = true;
648
649 // ─────────────────────────────────────────────────────────────────────
650 // STEP 2: Distribute TOKENS to fixed recipients
651 // ─────────────────────────────────────────────────────────────────────
652
653 // Founder allocations
654 lockitToken.safeTransfer(founderWallet, FOUNDER_IMMEDIATE);
655 lockitToken.safeTransfer(founderVestingContract, FOUNDER_VESTING);
656
657 // DAO allocations (TOKENS only - no USDC to DAO per Services Agreement)
658 lockitToken.safeTransfer(daoTreasury, DAO_IMMEDIATE);
659 lockitToken.safeTransfer(daoVestingContract, DAO_VESTING);
660
661 // ─────────────────────────────────────────────────────────────────────
662 // STEP 3: Distribute USDC to operational wallets (blacklist-safe)
663 // v2.2 FIX: Removed daoTreasury USDC transfer - only 3 recipients now
664 // ─────────────────────────────────────────────────────────────────────
665
666 _safeUSDCTransferOrFallback(complianceWallet, COMPLIANCE_USDC_ALLOCATION);
667 _safeUSDCTransferOrFallback(devWallet, DEV_USDC_ALLOCATION);
668 _safeUSDCTransferOrFallback(infraWallet, INFRA_USDC_ALLOCATION);
669
670 emit SaleFinalized(totalRaised, contributors.length, lpTokensBurned);
671 }
672
673 /**
674 * @notice Execute manual override after LP creation has failed multiple times
675 * @dev ONLY lpOpsWallet can call this (prevents griefing attacks)
676 * Sends LP allocation to lpOpsWallet for manual LP creation
677 * Requires full balances intact and blocks if emergency window open
678 */
679 function executeManualOverride() external nonReentrant {
680 // CRITICAL: Only lpOpsWallet can execute override
681 // This prevents third parties from forcing the sale into 90% refund mode
682 if (msg.sender != lpOpsWallet) revert OnlyLpOpsWallet();
683
684 if (!manualOverrideEnabled) revert ManualOverrideNotEnabled();
685 if (manualOverrideExecuted) revert ManualOverrideAlreadyExecuted();
686 if (finalized) revert AlreadyFinalized();
687
688 // Prevent execution if emergency refund window is open
689 // This prevents the "partial refunds then override" trap
690 if (goalReachedAt != 0 && block.timestamp >= goalReachedAt + EMERGENCY_REFUND_DELAY) {
691 revert EmergencyWindowOpen();
692 }
693
694 // Require full balances (ensures no refunds have occurred)
695 if (usdc.balanceOf(address(this)) < GOAL) revert InsufficientUSDCBalance();
696 if (lockitToken.balanceOf(address(this)) < TOTAL_SUPPLY) revert InsufficientTokenBalance();
697
698 manualOverrideExecuted = true;
699 manualOverrideExecutedAt = block.timestamp;
700
701 // Send LP allocation to lpOpsWallet for manual LP creation
702 // This allows fast action without waiting for governance
703 lockitToken.safeTransfer(lpOpsWallet, LP_TOKEN_ALLOCATION);
704 usdc.safeTransfer(lpOpsWallet, LP_USDC_ALLOCATION);
705
706 emit ManualOverrideExecuted(LP_TOKEN_ALLOCATION, LP_USDC_ALLOCATION, block.timestamp);
707 }
708
709 /**
710 * @notice Complete finalization after manual override
711 * @dev Distributes remaining tokens and USDC after LP was manually created
712 * Anyone can call once override has been executed
713 * REQUIRES: LP tokens have been burned to BURN_ADDRESS (minimum threshold enforced)
714 * Uses blacklist-safe transfers for all USDC payouts
715 * BLOCKED if any override escape refunds have been claimed
716 *
717 * v2.2 CHANGE: Removed USDC transfer to daoTreasury
718 */
719 function completeFinalization() external nonReentrant {
720 if (!manualOverrideExecuted) revert ManualOverrideNotExecuted();
721 if (finalized) revert AlreadyFinalized();
722
723 // Block if escape refunds have started - prevents mixed state
724 if (overrideEscapeActivated) revert OverrideEscapeActivated();
725
726 // ─────────────────────────────────────────────────────────────────────
727 // VERIFY LP WAS CREATED AND PROPERLY BURNED
728 // ─────────────────────────────────────────────────────────────────────
729 address pair = IUniswapV2Factory(uniswapFactory).getPair(address(lockitToken), address(usdc));
730 if (pair == address(0)) revert LPNotBurned();
731
732 // v2: Use distinct error for token mismatch
733 address t0 = IUniswapV2Pair(pair).token0();
734 address t1 = IUniswapV2Pair(pair).token1();
735 bool tokensMatch = (t0 == address(lockitToken) && t1 == address(usdc)) ||
736 (t0 == address(usdc) && t1 == address(lockitToken));
737 if (!tokensMatch) revert PairTokenMismatch();
738
739 // Verify LP tokens exist and substantial amount is burned
740 // Prevents "burn 1 wei and keep the rest" attack
741 uint256 burnedLP = IUniswapV2Pair(pair).balanceOf(BURN_ADDRESS);
742 uint256 minExpectedLP = _minExpectedInitialLiquidity();
743 if (burnedLP < minExpectedLP) revert InsufficientLPBurned();
744
745 // Store pair address for reference
746 lpPair = pair;
747
748 // ─────────────────────────────────────────────────────────────────────
749 // VERIFY REMAINING BALANCES
750 // v2.2 FIX: Updated required USDC calculation (no DAO allocation)
751 // ─────────────────────────────────────────────────────────────────────
752 uint256 requiredUSDC = GOAL - LP_USDC_ALLOCATION; // $450k
753 if (usdc.balanceOf(address(this)) < requiredUSDC) revert InsufficientUSDCBalance();
754
755 uint256 requiredLOCKIT = TOTAL_SUPPLY - LP_TOKEN_ALLOCATION; // 950M
756 if (lockitToken.balanceOf(address(this)) < requiredLOCKIT) revert InsufficientTokenBalance();
757
758 finalized = true;
759
760 // Distribute TOKENS to fixed recipients
761 lockitToken.safeTransfer(founderWallet, FOUNDER_IMMEDIATE);
762 lockitToken.safeTransfer(founderVestingContract, FOUNDER_VESTING);
763 lockitToken.safeTransfer(daoTreasury, DAO_IMMEDIATE);
764 lockitToken.safeTransfer(daoVestingContract, DAO_VESTING);
765
766 // Distribute remaining USDC (LP portion already sent in override) - blacklist-safe
767 // v2.2 FIX: Removed daoTreasury USDC transfer
768 _safeUSDCTransferOrFallback(complianceWallet, COMPLIANCE_USDC_ALLOCATION);
769 _safeUSDCTransferOrFallback(devWallet, DEV_USDC_ALLOCATION);
770 _safeUSDCTransferOrFallback(infraWallet, INFRA_USDC_ALLOCATION);
771
772 emit FinalizationCompleted(totalRaised, contributors.length);
773 }
774
775 /**
776 * @notice Calculate minimum expected LP tokens from a full first mint
777 * @dev Uniswap V2 first mint: liquidity = sqrt(amount0 * amount1) - MINIMUM_LIQUIDITY
778 * This is used to verify manual override burned sufficient LP
779 * @return Minimum expected LP tokens
780 */
781 function _minExpectedInitialLiquidity() internal pure returns (uint256) {
782 // sqrt(50M tokens * 50k USDC) - 1000
783 // Note: different decimals but Math.sqrt handles the product correctly
784 uint256 root = Math.sqrt(LP_TOKEN_ALLOCATION * LP_USDC_ALLOCATION);
785 return root - UNISWAP_MINIMUM_LIQUIDITY;
786 }
787
788 /**
789 * @notice Try to create Uniswap V2 LP and burn the LP tokens
790 * @dev ATOMIC: All operations (transfer + mint + burn) are wrapped in an external
791 * self-call so they revert together if any step fails.
792 * This prevents the "tokens stranded in pair" brick scenario.
793 *
794 * For uninitialized pairs (totalSupply == 0):
795 * - Transfer tokens directly to pair, call mint()
796 * - Uniswap V2 first mint uses sqrt(amount0 * amount1) where amount = balance - reserve
797 * - Even if attacker sync'd dust, our amounts (50M + $50k) dominate
798 * - Attacker's dust gets absorbed, our ratio is preserved
799 *
800 * For initialized pairs (totalSupply > 0):
801 * - Someone already minted LP - this is a fundamental problem
802 * - Fail safely into refund/override path
803 *
804 * v2 FIX: If createPair() fails, re-check getPair() to handle front-running
805 *
806 * @return success Whether LP creation succeeded
807 * @return lpTokensBurned Amount of LP tokens burned (0 if failed)
808 * @return failReason Human-readable failure reason
809 */
810 function _tryCreateAndBurnLP() internal returns (bool success, uint256 lpTokensBurned, string memory failReason) {
811 // ─────────────────────────────────────────────────────────────────────
812 // STEP 1: Get or create pair
813 // v2 FIX: Handle createPair() front-running by re-checking getPair()
814 // ─────────────────────────────────────────────────────────────────────
815 address pair = IUniswapV2Factory(uniswapFactory).getPair(address(lockitToken), address(usdc));
816
817 if (pair == address(0)) {
818 // Try to create the pair
819 try IUniswapV2Factory(uniswapFactory).createPair(address(lockitToken), address(usdc)) returns (address newPair) {
820 pair = newPair;
821 } catch {
822 // v2 FIX: createPair failed - maybe someone front-ran us
823 // Re-check getPair() to see if pair now exists
824 pair = IUniswapV2Factory(uniswapFactory).getPair(address(lockitToken), address(usdc));
825 if (pair == address(0)) {
826 // Pair truly doesn't exist and we can't create it
827 return (false, 0, "Pair creation failed");
828 }
829 // Pair exists now (someone else created it) - continue with it
830 }
831 }
832
833 // Store pair address
834 lpPair = pair;
835
836 // ─────────────────────────────────────────────────────────────────────
837 // STEP 2: Verify pair contains expected tokens
838 // ─────────────────────────────────────────────────────────────────────
839 address t0 = IUniswapV2Pair(pair).token0();
840 address t1 = IUniswapV2Pair(pair).token1();
841 bool tokensMatch = (t0 == address(lockitToken) && t1 == address(usdc)) ||
842 (t0 == address(usdc) && t1 == address(lockitToken));
843 if (!tokensMatch) {
844 return (false, 0, "Pair token mismatch");
845 }
846
847 // ─────────────────────────────────────────────────────────────────────
848 // STEP 3: Check if pair is already initialized
849 // ─────────────────────────────────────────────────────────────────────
850 uint256 existingSupply = IUniswapV2Pair(pair).totalSupply();
851
852 if (existingSupply > 0) {
853 // Pair already has LP tokens - someone already minted
854 // This is a fundamental issue - fail into refund/override path
855 return (false, 0, "Pair already has LP tokens");
856 }
857
858 // ─────────────────────────────────────────────────────────────────────
859 // STEP 4: Atomic mint and burn via self-call
860 // CRITICAL: This ensures if mint() fails, the transfers revert too
861 // ─────────────────────────────────────────────────────────────────────
862 try this.atomicMintAndBurnLP(pair) returns (uint256 liquidity) {
863 return (true, liquidity, "");
864 } catch Error(string memory reason) {
865 return (false, 0, reason);
866 } catch {
867 return (false, 0, "Mint/burn failed");
868 }
869 }
870
871 /**
872 * @notice Atomically transfer tokens to pair, mint LP, and burn LP
873 * @dev EXTERNAL so try/catch works - only callable by this contract itself.
874 * If any step fails, the entire call reverts, protecting funds.
875 * @param pair The Uniswap V2 pair address
876 * @return liquidity Amount of LP tokens minted and burned
877 */
878 function atomicMintAndBurnLP(address pair) external returns (uint256 liquidity) {
879 // Only callable by this contract
880 if (msg.sender != address(this)) revert OnlySelf();
881
882 // Transfer both tokens to pair
883 lockitToken.safeTransfer(pair, LP_TOKEN_ALLOCATION);
884 usdc.safeTransfer(pair, LP_USDC_ALLOCATION);
885
886 // Mint LP to this vault
887 liquidity = IUniswapV2Pair(pair).mint(address(this));
888 if (liquidity == 0) revert ZeroLiquidityMinted();
889
890 emit LiquidityCreated(pair, liquidity);
891
892 // Burn LP by sending to dead address
893 IERC20(pair).safeTransfer(BURN_ADDRESS, liquidity);
894
895 emit LPTokensBurned(pair, liquidity);
896 }
897
898 // ═══════════════════════════════════════════════════════════════════════════
899 // TOKEN DISTRIBUTION (pull-based claim + batched push)
900 // ═══════════════════════════════════════════════════════════════════════════
901
902 /**
903 * @notice Claim your LOCKIT tokens after finalization (pull-based)
904 * @dev Contributors can call this directly without waiting for batch distribution
905 */
906 function claim() external nonReentrant {
907 if (!finalized) revert NotFinalized();
908 if (tokensReceived[msg.sender]) revert AlreadyClaimed();
909
910 uint256 contribution = contributions[msg.sender];
911 if (contribution == 0) revert NoContribution();
912
913 tokensReceived[msg.sender] = true;
914 uint256 tokenAmount = _calculateTokens(contribution);
915 lockitToken.safeTransfer(msg.sender, tokenAmount);
916
917 emit TokensClaimed(msg.sender, tokenAmount);
918 }
919
920 /**
921 * @notice Distribute tokens to contributors in batches (push-based)
922 * @dev Anyone can call this to push tokens. Caller pays gas.
923 * @param count Number of contributors to process in this batch
924 */
925 function distribute(uint256 count) external nonReentrant {
926 if (!finalized) revert NotFinalized();
927 if (distributionIndex >= contributors.length) revert DistributionComplete();
928
929 _distributeInternal(count);
930 }
931
932 /**
933 * @notice Internal distribution logic
934 */
935 function _distributeInternal(uint256 count) internal {
936 uint256 startIndex = distributionIndex;
937 uint256 endIndex = startIndex + count;
938 if (endIndex > contributors.length) {
939 endIndex = contributors.length;
940 }
941
942 uint256 totalDistributed = 0;
943
944 for (uint256 i = startIndex; i < endIndex; i++) {
945 address contributor = contributors[i];
946
947 // Skip if already received (via claim() or previous distribution)
948 if (tokensReceived[contributor]) continue;
949
950 uint256 contribution = contributions[contributor];
951
952 // Skip zero contributions (from refunded contributors)
953 if (contribution == 0) continue;
954
955 uint256 tokenAmount = _calculateTokens(contribution);
956
957 tokensReceived[contributor] = true;
958 lockitToken.safeTransfer(contributor, tokenAmount);
959
960 totalDistributed += tokenAmount;
961 emit TokensDistributed(contributor, tokenAmount);
962 }
963
964 distributionIndex = endIndex;
965 emit BatchDistributed(startIndex, endIndex, totalDistributed);
966 }
967
968 /**
969 * @notice Distribute tokens to all remaining contributors
970 * @dev Convenience function - may run out of gas with many contributors
971 * If this reverts due to gas, use distribute(count) in batches
972 */
973 function distributeAll() external nonReentrant {
974 if (!finalized) revert NotFinalized();
975 if (distributionIndex >= contributors.length) revert DistributionComplete();
976
977 _distributeInternal(contributors.length - distributionIndex);
978 }
979
980 /**
981 * @notice Calculate tokens for a given USDC contribution
982 * @param usdcAmount USDC contribution (6 decimals)
983 * @return Token amount (18 decimals)
984 */
985 function _calculateTokens(uint256 usdcAmount) internal pure returns (uint256) {
986 // 1000 tokens per $1 USDC
987 // usdcAmount is in 1e6, tokens are in 1e18
988 // tokens = usdcAmount * 1000e18 / 1e6 = usdcAmount * 1000e12
989 return usdcAmount * TOKENS_PER_USDC / 1e6;
990 }
991
992 // ═══════════════════════════════════════════════════════════════════════════
993 // REFUNDS (if goal not met or emergency situations)
994 // ═══════════════════════════════════════════════════════════════════════════
995
996 /**
997 * @notice Claim refund if sale failed or emergency conditions are met
998 * @dev Two refund paths:
999 * 1. Normal: deadline passed AND goal not met
1000 * 2. Emergency: goal met but finalize stuck for 180 days after goalReachedAt
1001 * (INDEPENDENT of deadline - uses goalReachedAt timing)
1002 * Emergency refunds blocked if manual override executed (use overrideEscapeRefund instead)
1003 */
1004 function refund() external nonReentrant {
1005 if (finalized) revert SaleEnded();
1006
1007 // Block if manual override was executed - must use overrideEscapeRefund instead
1008 if (manualOverrideExecuted) revert ManualOverrideAlreadyExecuted();
1009
1010 // Determine refund eligibility
1011 // Normal refund: deadline passed AND goal not met
1012 bool normalRefund = block.timestamp >= deadline && totalRaised < GOAL;
1013
1014 // Emergency refund: goal met but finalize stuck for 180 days
1015 // INDEPENDENT of deadline - this is the fix for the timing bug
1016 bool emergencyRefund = goalReachedAt != 0 &&
1017 block.timestamp >= goalReachedAt + EMERGENCY_REFUND_DELAY;
1018
1019 // Must qualify for either normal or emergency refund
1020 if (!normalRefund && !emergencyRefund) revert NotRefundable();
1021
1022 uint256 contribution = contributions[msg.sender];
1023 if (contribution == 0) revert NoContribution();
1024
1025 // Clear contribution state to prevent any future claims
1026 contributions[msg.sender] = 0;
1027 totalRaised -= contribution;
1028
1029 usdc.safeTransfer(msg.sender, contribution);
1030
1031 emit RefundClaimed(msg.sender, contribution);
1032 }
1033
1034 /**
1035 * @notice Emergency escape refund after manual override was executed but not completed
1036 * @dev If manual override was executed but completeFinalization() hasn't happened
1037 * after 90 days, contributors can claim prorated refunds from remaining USDC.
1038 * This prevents permanent stuck state if completeFinalization() fails.
1039 *
1040 * Prorated amount = contribution * (remaining USDC / original USDC before override)
1041 * Since LP allocation ($50k) was sent out, remaining is $450k of original $500k = 90%
1042 *
1043 * IMPORTANT: Once any escape refund is claimed, this activates overrideEscapeActivated
1044 * which permanently blocks completeFinalization() to prevent mixed states.
1045 * Contributors are fully exited (contribution zeroed, tokensReceived marked).
1046 */
1047 function overrideEscapeRefund() external nonReentrant {
1048 // Must be in override-executed-but-not-completed state
1049 if (!manualOverrideExecuted) revert ManualOverrideNotExecuted();
1050 if (finalized) revert SaleEnded();
1051
1052 // Must wait 90 days after override was executed
1053 if (block.timestamp < manualOverrideExecutedAt + OVERRIDE_ESCAPE_DELAY) {
1054 revert OverrideEscapeNotAvailable();
1055 }
1056
1057 // Check contribution
1058 uint256 contribution = contributions[msg.sender];
1059 if (contribution == 0) revert NoContribution();
1060
1061 // Prevent double claim (check tokensReceived as well for defense in depth)
1062 if (tokensReceived[msg.sender]) revert AlreadyClaimed();
1063
1064 // Calculate prorated refund (90% since LP allocation was sent out)
1065 // This is (contribution * 450k / 500k) = contribution * 9 / 10
1066 uint256 proRataRefund = (contribution * (GOAL - LP_USDC_ALLOCATION)) / GOAL;
1067
1068 // Require sufficient balance - revert if vault doesn't have enough
1069 // This prevents the "partial refund then marked as claimed" fairness issue
1070 uint256 available = usdc.balanceOf(address(this));
1071 if (available < proRataRefund) revert InsufficientUSDCBalance();
1072
1073 // Activate escape mode on first escape refund - blocks completeFinalization() forever
1074 if (!overrideEscapeActivated) {
1075 overrideEscapeActivated = true;
1076 }
1077
1078 // Fully exit the contributor from the sale
1079 // This prevents double-dipping (refund now + claim tokens later)
1080 contributions[msg.sender] = 0;
1081 tokensReceived[msg.sender] = true; // Defense in depth - blocks claim() too
1082 totalRaised -= contribution; // Keep accounting clean
1083
1084 usdc.safeTransfer(msg.sender, proRataRefund);
1085
1086 emit OverrideEscapeRefundClaimed(msg.sender, proRataRefund);
1087 }
1088
1089 // ═══════════════════════════════════════════════════════════════════════════
1090 // TOKEN RESCUE (for accidentally sent tokens - NOT LOCKIT or USDC)
1091 // ═══════════════════════════════════════════════════════════════════════════
1092
1093 /**
1094 * @notice Rescue accidentally sent tokens (NOT LOCKIT or USDC)
1095 * @dev Anyone can call - tokens go to DAO treasury
1096 * LOCKIT and USDC are intentionally non-rescuable to maintain trust model
1097 * @param _token Address of token to rescue
1098 */
1099 function rescueTokens(address _token) external nonReentrant {
1100 if (_token == address(lockitToken)) revert CannotRescueLOCKIT();
1101 if (_token == address(usdc)) revert CannotRescueUSDC();
1102
1103 IERC20 token = IERC20(_token);
1104 uint256 balance = token.balanceOf(address(this));
1105 if (balance == 0) revert NoTokensToRescue();
1106
1107 token.safeTransfer(daoTreasury, balance);
1108
1109 emit TokensRescued(_token, balance, daoTreasury);
1110 }
1111
1112 // ═══════════════════════════════════════════════════════════════════════════
1113 // VIEW FUNCTIONS
1114 // ═══════════════════════════════════════════════════════════════════════════
1115
1116 /**
1117 * @notice Get the number of contributors
1118 */
1119 function getContributorCount() external view returns (uint256) {
1120 return contributors.length;
1121 }
1122
1123 /**
1124 * @notice Check if sale is ready (tokens deposited)
1125 * @dev v2: Returns true after finalization (was ready, now complete)
1126 */
1127 function isSaleReady() external view returns (bool ready, uint256 currentBalance, uint256 required) {
1128 uint256 balance = lockitToken.balanceOf(address(this));
1129 bool isReady = _isSaleReady();
1130 return (isReady, balance, TOTAL_SUPPLY);
1131 }
1132
1133 /**
1134 * @notice Get contribution for an address
1135 */
1136 function getContribution(address contributor) external view returns (uint256) {
1137 return contributions[contributor];
1138 }
1139
1140 /**
1141 * @notice Calculate tokens that will be received for a contribution amount
1142 */
1143 function calculateTokensForContribution(uint256 usdcAmount) external pure returns (uint256) {
1144 return _calculateTokens(usdcAmount);
1145 }
1146
1147 /**
1148 * @notice Get remaining capacity before goal is met
1149 */
1150 function getRemainingCapacity() external view returns (uint256) {
1151 if (totalRaised >= GOAL) return 0;
1152 return GOAL - totalRaised;
1153 }
1154
1155 /**
1156 * @notice Check if an address can still contribute and how much
1157 * @dev v2: Returns 0 if goalReachedAt != 0 (goal ever reached = closed forever)
1158 */
1159 function getContributionRoom(address contributor) external view returns (uint256) {
1160 // Use the comprehensive check
1161 if (!_canAcceptContributions()) return 0;
1162
1163 uint256 currentContribution = contributions[contributor];
1164 uint256 walletRoom = MAX_CONTRIBUTION - currentContribution;
1165 uint256 saleRoom = GOAL - totalRaised;
1166
1167 return walletRoom < saleRoom ? walletRoom : saleRoom;
1168 }
1169
1170 /**
1171 * @notice Get the minimum contribution amount for current state
1172 * @dev Returns the smaller of MIN_CONTRIBUTION or remaining capacity
1173 * v2: Returns 0 if goal ever reached
1174 */
1175 function getEffectiveMinContribution() external view returns (uint256) {
1176 if (!_canAcceptContributions()) return 0;
1177
1178 uint256 remaining = GOAL - totalRaised;
1179 return remaining < MIN_CONTRIBUTION ? remaining : MIN_CONTRIBUTION;
1180 }
1181
1182 /**
1183 * @notice Get claimable token amount for an address
1184 * @dev Returns 0 if not finalized, already claimed, or no contribution
1185 */
1186 function getClaimableAmount(address contributor) external view returns (uint256) {
1187 if (!finalized) return 0;
1188 if (tokensReceived[contributor]) return 0;
1189
1190 uint256 contribution = contributions[contributor];
1191 if (contribution == 0) return 0;
1192
1193 return _calculateTokens(contribution);
1194 }
1195
1196 /**
1197 * @notice Get distribution progress
1198 */
1199 function getDistributionProgress() external view returns (
1200 uint256 distributed,
1201 uint256 total,
1202 bool complete
1203 ) {
1204 return (
1205 distributionIndex,
1206 contributors.length,
1207 distributionIndex >= contributors.length
1208 );
1209 }
1210
1211 /**
1212 * @notice Check if a contributor has received their tokens
1213 */
1214 function hasReceivedTokens(address contributor) external view returns (bool) {
1215 return tokensReceived[contributor];
1216 }
1217
1218 /**
1219 * @notice Get time remaining until deadline
1220 */
1221 function getTimeRemaining() external view returns (uint256) {
1222 if (block.timestamp >= deadline) return 0;
1223 return deadline - block.timestamp;
1224 }
1225
1226 /**
1227 * @notice Check refund eligibility for an address
1228 * @dev Emergency refunds available 180 days after goal reached (INDEPENDENT of deadline)
1229 * Emergency refunds blocked if manual override was executed (use getOverrideEscapeStatus)
1230 * @return canRefund Whether the address can currently claim a refund via refund()
1231 * @return isEmergency Whether this would be an emergency refund
1232 * @return contribution Amount that would be refunded
1233 * @return timeUntilRefundable Seconds until some refund becomes available (0 if available now)
1234 */
1235 function getRefundStatus(address contributor) external view returns (
1236 bool canRefund,
1237 bool isEmergency,
1238 uint256 contribution,
1239 uint256 timeUntilRefundable
1240 ) {
1241 contribution = contributions[contributor];
1242
1243 // Can't refund if finalized, no contribution, or manual override executed
1244 if (finalized || contribution == 0 || manualOverrideExecuted) {
1245 return (false, false, contribution, 0);
1246 }
1247
1248 // Normal refund: deadline passed AND goal not met
1249 if (block.timestamp >= deadline && totalRaised < GOAL) {
1250 return (true, false, contribution, 0);
1251 }
1252
1253 // Emergency refund: goal met but finalize stuck for 180 days (independent of deadline)
1254 uint256 emergencyTime = goalReachedAt != 0 ? goalReachedAt + EMERGENCY_REFUND_DELAY : 0;
1255 if (emergencyTime != 0 && block.timestamp >= emergencyTime) {
1256 return (true, true, contribution, 0);
1257 }
1258
1259 // Calculate time until some refund is available
1260 if (totalRaised < GOAL) {
1261 // Waiting for deadline (normal refund path)
1262 timeUntilRefundable = block.timestamp < deadline ? deadline - block.timestamp : 0;
1263 } else if (emergencyTime != 0) {
1264 // Waiting for emergency window
1265 timeUntilRefundable = block.timestamp < emergencyTime ? emergencyTime - block.timestamp : 0;
1266 }
1267
1268 return (false, false, contribution, timeUntilRefundable);
1269 }
1270
1271 /**
1272 * @notice Check override escape refund eligibility
1273 * @return canClaim Whether override escape refund is available
1274 * @return proRataAmount Refund amount (90% of contribution)
1275 * @return timeUntilAvailable Seconds until escape becomes available (0 if available now)
1276 */
1277 function getOverrideEscapeStatus(address contributor) external view returns (
1278 bool canClaim,
1279 uint256 proRataAmount,
1280 uint256 timeUntilAvailable
1281 ) {
1282 uint256 contribution = contributions[contributor];
1283
1284 // Calculate prorated amount
1285 proRataAmount = (contribution * (GOAL - LP_USDC_ALLOCATION)) / GOAL;
1286
1287 // Check conditions - tokensReceived also blocks (defense in depth)
1288 if (!manualOverrideExecuted || finalized || contribution == 0 || tokensReceived[contributor]) {
1289 return (false, proRataAmount, 0);
1290 }
1291
1292 uint256 escapeTime = manualOverrideExecutedAt + OVERRIDE_ESCAPE_DELAY;
1293 if (block.timestamp >= escapeTime) {
1294 return (true, proRataAmount, 0);
1295 }
1296
1297 return (false, proRataAmount, escapeTime - block.timestamp);
1298 }
1299
1300 /**
1301 * @notice Get finalization and manual override status
1302 * @return attempts Number of failed finalize attempts
1303 * @return lastBlock Block number of last attempt
1304 * @return blocksUntilRetry Blocks remaining before next attempt allowed (0 if ready)
1305 * @return overrideEnabled Whether manual override is available
1306 * @return overrideExecuted Whether manual override has been used
1307 * @return overrideExecutedAt_ Timestamp when override was executed (0 if not)
1308 * @return escapeActivated Whether override escape refunds have started (blocks completeFinalization)
1309 * @return isFinalized Whether sale has been finalized
1310 */
1311 function getFinalizeStatus() external view returns (
1312 uint256 attempts,
1313 uint256 lastBlock,
1314 uint256 blocksUntilRetry,
1315 bool overrideEnabled,
1316 bool overrideExecuted,
1317 uint256 overrideExecutedAt_,
1318 bool escapeActivated,
1319 bool isFinalized
1320 ) {
1321 attempts = failedAttempts;
1322 lastBlock = lastAttemptBlock;
1323
1324 if (failedAttempts > 0 && block.number < lastAttemptBlock + MIN_BLOCKS_BETWEEN_ATTEMPTS) {
1325 blocksUntilRetry = (lastAttemptBlock + MIN_BLOCKS_BETWEEN_ATTEMPTS) - block.number;
1326 } else {
1327 blocksUntilRetry = 0;
1328 }
1329
1330 overrideEnabled = manualOverrideEnabled;
1331 overrideExecuted = manualOverrideExecuted;
1332 overrideExecutedAt_ = manualOverrideExecutedAt;
1333 escapeActivated = overrideEscapeActivated;
1334 isFinalized = finalized;
1335 }
1336
1337 /**
1338 * @notice Get sale status
1339 * @dev v2: isOpen properly reflects monotonic lifecycle (closed once goal reached)
1340 */
1341 function getSaleStatus() external view returns (
1342 bool isReady,
1343 bool isOpen,
1344 bool isFinalized,
1345 bool goalMet,
1346 bool deadlinePassed,
1347 uint256 raised,
1348 uint256 goal,
1349 uint256 contributorCount
1350 ) {
1351 bool _isReady = _isSaleReady();
1352 bool _goalMet = goalReachedAt != 0; // v2: Use goalReachedAt for definitive "goal ever met"
1353 bool _deadlinePassed = block.timestamp >= deadline;
1354 bool _isOpen = _canAcceptContributions(); // v2: Use comprehensive check
1355
1356 return (
1357 _isReady,
1358 _isOpen,
1359 finalized,
1360 _goalMet,
1361 _deadlinePassed,
1362 totalRaised,
1363 GOAL,
1364 contributors.length
1365 );
1366 }
1367
1368 /**
1369 * @notice Get all recipient addresses for verification
1370 * @dev v2.2: Renamed legalWallet → complianceWallet
1371 */
1372 function getRecipients() external view returns (
1373 address founder,
1374 address founderVesting,
1375 address dao,
1376 address daoVesting,
1377 address compliance,
1378 address dev,
1379 address infra,
1380 address lpOps
1381 ) {
1382 return (
1383 founderWallet,
1384 founderVestingContract,
1385 daoTreasury,
1386 daoVestingContract,
1387 complianceWallet,
1388 devWallet,
1389 infraWallet,
1390 lpOpsWallet
1391 );
1392 }
1393
1394 /**
1395 * @notice Get token allocation breakdown
1396 * @dev Useful for UI verification
1397 */
1398 function getTokenAllocations() external pure returns (
1399 uint256 sale,
1400 uint256 lp,
1401 uint256 founderImmediate,
1402 uint256 founderVesting,
1403 uint256 daoImmediate,
1404 uint256 daoVesting
1405 ) {
1406 return (
1407 SALE_ALLOCATION,
1408 LP_TOKEN_ALLOCATION,
1409 FOUNDER_IMMEDIATE,
1410 FOUNDER_VESTING,
1411 DAO_IMMEDIATE,
1412 DAO_VESTING
1413 );
1414 }
1415
1416 /**
1417 * @notice Get USDC allocation breakdown
1418 * @dev v2.2 FIX: Removed DAO allocation, renamed legal → compliance
1419 */
1420 function getUSDCAllocations() external pure returns (
1421 uint256 lp,
1422 uint256 compliance,
1423 uint256 dev,
1424 uint256 infra
1425 ) {
1426 return (
1427 LP_USDC_ALLOCATION,
1428 COMPLIANCE_USDC_ALLOCATION,
1429 DEV_USDC_ALLOCATION,
1430 INFRA_USDC_ALLOCATION
1431 );
1432 }
1433
1434 /**
1435 * @notice Get goal reached timestamp
1436 * @dev Returns 0 if goal not yet reached
1437 */
1438 function getGoalReachedAt() external view returns (uint256) {
1439 return goalReachedAt;
1440 }
1441
1442 /**
1443 * @notice Get LP pair address
1444 * @dev Returns address(0) if LP not yet created
1445 */
1446 function getLPPair() external view returns (address) {
1447 return lpPair;
1448 }
1449
1450 /**
1451 * @notice Get minimum expected LP tokens from first mint
1452 * @dev Used for verifying manual override burned sufficient LP
1453 */
1454 function getMinExpectedLP() external pure returns (uint256) {
1455 return _minExpectedInitialLiquidity();
1456 }
1457
1458 /**
1459 * @notice Get token price in USDC
1460 * @dev Returns the price per token in USDC with 6 decimals
1461 * Price = $0.001 per token = 1000 USDC units (1e3) per 1e18 tokens
1462 */
1463 function getTokenPrice() external pure returns (uint256 pricePerToken, string memory priceString) {
1464 // 1 token (1e18) costs 1e6 / 1000 = 1000 USDC units = $0.001
1465 return (1e6 / 1000, "$0.001");
1466 }
1467 }