@@ -21,8 +21,10 @@ import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol";
2121 * On the first deposit each address selects a delivery tranche, locked for the whole sale
2222 * (top-ups inherit it). The tranche does not affect accounting; it only records the choice.
2323 *
24- * Allocation is computed off-chain; this contract only holds deposits. Deposits are locked and
25- * can only be topped up, never withdrawn. Settlement and delivery are added via UUPS upgrade.
24+ * Allocation is computed off-chain; the contract holds deposits and, after the review window,
25+ * a finalized settlement merkle root drives claims. Deposits are locked and can only be topped
26+ * up, never withdrawn by the user. Claiming pays the refund and, for the unlocked tranche,
27+ * delivers the share token; the locked tranche only records the amount for off-chain delivery.
2628 */
2729contract PreIPODistributor is
2830 Initializable ,
@@ -35,7 +37,9 @@ contract PreIPODistributor is
3537 bytes32 public constant MANAGER = keccak256 ("MANAGER " );
3638 bytes32 public constant BOT = keccak256 ("BOT " );
3739
38- // Delivery tranche selected at first deposit; 0 = not selected.
40+ // Delivery tranche, selected at an account's first deposit and locked for the sale; 0 = not selected.
41+ // On claim, TRANCHE_UNLOCKED transfers the share token to the account immediately, while
42+ // TRANCHE_LOCKED only records the share amount (delivered off-chain at maturity).
3943 uint8 public constant TRANCHE_UNLOCKED = 1 ;
4044 uint8 public constant TRANCHE_LOCKED = 2 ;
4145
@@ -233,6 +237,11 @@ contract PreIPODistributor is
233237 {
234238 Sale storage sale = sales[_saleId];
235239 require (sale.whitelistRoot != bytes32 (0 ), "Invalid saleId " );
240+ // cannot open/reschedule a public round once settlement is pending or finalized
241+ require (
242+ settlements[_saleId].pendingRoot == bytes32 (0 ) && settlements[_saleId].root == bytes32 (0 ),
243+ "Settlement started "
244+ );
236245 require (sale.pubStartTime == 0 || sale.pubStartTime > block .timestamp , "Public round started " );
237246 require (_pubStartTime > sale.endTime, "Public must follow WL " );
238247 require (_pubStartTime > block .timestamp , "Invalid pub start " );
@@ -265,14 +274,15 @@ contract PreIPODistributor is
265274 require (_amount >= sale.minDeposit, "Below min deposit " );
266275
267276 // A non-zero existing WL deposit implies the caller already passed the proof check.
268- if (deposits[_saleId][msg .sender ] == 0 ) {
277+ uint256 prior = deposits[_saleId][msg .sender ];
278+ if (prior == 0 ) {
269279 bytes32 leaf = keccak256 (abi.encode (block .chainid , msg .sender ));
270280 require (MerkleProof.verifyCalldata (_proof, sale.whitelistRoot, leaf), "Invalid proof " );
271281 }
272282
273283 uint8 tranche = _applyTranche (_saleId, _tranche);
274284
275- uint256 userTotal = deposits[_saleId][ msg . sender ] + _amount;
285+ uint256 userTotal = prior + _amount;
276286 deposits[_saleId][msg .sender ] = userTotal;
277287 sale.totalDeposits += _amount;
278288
@@ -324,14 +334,15 @@ contract PreIPODistributor is
324334 {
325335 Sale storage sale = sales[_saleId];
326336 require (sale.whitelistRoot != bytes32 (0 ), "Invalid saleId " );
337+ require (! sale.paused, "Sale paused " );
327338 // deposit windows must be closed so no deposit can land after the off-chain snapshot
328339 require (block .timestamp > sale.endTime, "WL round not ended " );
329340 require (sale.pubStartTime == 0 || block .timestamp > sale.pubEndTime, "Public round not ended " );
330341
331342 Settlement storage s = settlements[_saleId];
332343 // settlement is single-shot: once a root is finalized it cannot be replaced
333344 require (s.root == bytes32 (0 ), "Already finalized " );
334- require (_root != bytes32 (0 ) && _root != s.pendingRoot , "Invalid root " );
345+ require (_root != bytes32 (0 ), "Invalid root " );
335346 require (s.pendingRoot == bytes32 (0 ), "Pending root in flight " );
336347 require (_totalRefund <= sale.totalDeposits + sale.pubTotalDeposits, "Refund exceeds deposits " );
337348
@@ -344,6 +355,7 @@ contract PreIPODistributor is
344355
345356 /// @dev Finalize the pending settlement root (step 2 of 2); only after the review window.
346357 function finalizeSettlement (uint64 _saleId ) external onlyRole (BOT) {
358+ require (! sales[_saleId].paused, "Sale paused " );
347359 Settlement storage s = settlements[_saleId];
348360 require (s.pendingRoot != bytes32 (0 ), "No pending root " );
349361 require (block .timestamp >= s.lastSetTime + waitingPeriod, "Review window not passed " );
@@ -368,8 +380,14 @@ contract PreIPODistributor is
368380 /// @dev Claim a finalized allocation (whitelist + public rounds combined), once per account.
369381 /// Pays the refund; for the unlocked tranche also transfers the share token, while the
370382 /// locked tranche only records the amount (delivered off-chain at maturity). Permanent.
383+ /// Permissionless: anyone may call it on behalf of any account; the refund and any share
384+ /// delivery always go to `_account` (the address bound in the leaf), never to msg.sender.
385+ /// @param _saleId Sale id
386+ /// @param _account Account the allocation belongs to and that receives refund/shares
387+ /// @param _refundAmount Refund amount in the deposit token (part of the leaf)
371388 /// @param _shareToken Share token to deliver for the unlocked tranche (part of the leaf)
372- /// @param _tokenAmount Share amount (delivered for unlocked, recorded for locked)
389+ /// @param _tokenAmount Share amount (delivered for unlocked, recorded for locked; part of the leaf)
390+ /// @param _proof Merkle proof of the leaf against the finalized settlement root
373391 function claim (
374392 uint64 _saleId ,
375393 address _account ,
@@ -378,6 +396,7 @@ contract PreIPODistributor is
378396 uint256 _tokenAmount ,
379397 bytes32 [] calldata _proof
380398 ) external nonReentrant {
399+ require (! sales[_saleId].paused, "Sale paused " );
381400 Settlement storage s = settlements[_saleId];
382401 require (s.root != bytes32 (0 ), "Not finalized " );
383402 require (! claimed[_saleId][_account], "Already claimed " );
@@ -409,6 +428,31 @@ contract PreIPODistributor is
409428 emit Claimed (_saleId, _account, _refundAmount, _shareToken, _tokenAmount, tranche);
410429 }
411430
431+ /// @dev Read-only preview of what claim() would do for the given leaf inputs, without changing
432+ /// state. Mirrors claim()'s validation and delivery routing.
433+ /// @return valid True if the sale is finalized and the proof matches the leaf
434+ /// @return alreadyClaimed True if the account has already claimed
435+ /// @return tranche The account's locked tranche (0 = never deposited)
436+ /// @return sendShares True if a successful claim would transfer the share token now
437+ /// (unlocked tranche, non-zero amount and share token)
438+ function previewClaim (
439+ uint64 _saleId ,
440+ address _account ,
441+ uint256 _refundAmount ,
442+ address _shareToken ,
443+ uint256 _tokenAmount ,
444+ bytes32 [] calldata _proof
445+ ) external view returns (bool valid , bool alreadyClaimed , uint8 tranche , bool sendShares ) {
446+ bytes32 leaf =
447+ keccak256 (abi.encode (block .chainid , _saleId, _account, _refundAmount, _shareToken, _tokenAmount));
448+ bytes32 root = settlements[_saleId].root;
449+ valid = root != bytes32 (0 ) && MerkleProof.verifyCalldata (_proof, root, leaf);
450+ alreadyClaimed = claimed[_saleId][_account];
451+ tranche = userTranche[_saleId][_account];
452+ sendShares =
453+ valid && ! alreadyClaimed && tranche == TRANCHE_UNLOCKED && _shareToken != address (0 ) && _tokenAmount > 0 ;
454+ }
455+
412456 /// @dev Update the review window between set and finalize (min 6h).
413457 function setWaitingPeriod (uint256 _waitingPeriod ) external onlyRole (MANAGER) {
414458 require (_waitingPeriod >= 6 hours, "Waiting period too short " );
@@ -417,6 +461,11 @@ contract PreIPODistributor is
417461 }
418462
419463 /// @dev Manager (multisig) safety valve; there is no normal withdrawal path.
464+ /// WARNING: this does NOT adjust any internal accounting (totalDeposits, pubTotalDeposits,
465+ /// settlement totals). Withdrawing the deposit token can leave the contract without enough
466+ /// balance to satisfy outstanding refunds, causing finalized claims to revert and stranding
467+ /// those refunds. After using it, the tracked totals must be restaged (or the contract
468+ /// upgraded) before normal claims can resume. Use only in emergencies.
420469 function emergencyWithdraw (address _token , address _to , uint256 _amount )
421470 external
422471 onlyRole (MANAGER)
0 commit comments