Skip to content

Feat/pre ipo claim - #119

Merged
qingyang-lista merged 3 commits into
masterfrom
feat/pre-ipo-claim
Jul 28, 2026
Merged

Feat/pre ipo claim#119
qingyang-lista merged 3 commits into
masterfrom
feat/pre-ipo-claim

Conversation

@razww

@razww razww commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds settlement + claim to PreIPODistributor, on top of the subscription contract. After the whitelist and public rounds close, the manager/bot publish a single settlement merkle root (both rounds combined) through a two-step, time-delayed lifecycle; users then make one permanent claim that pays their USDT refund and either delivers the share token (unlocked tranche) or records the amount for off-chain delivery (locked tranche). The allocation math (weighted pro-rata + cap + waterfall) stays off-chain — the contract only verifies and pays out results committed in the root.

What's added

  • Settlement root lifecycle (pending → accept, mirrors LendingRewardsDistributorV2):
    • setSettlementRoot(saleId, root, totalRefund)BOT, step 1; writes the pending root and starts the review window. Guards: root non-zero/new, no pending already in flight, and totalRefund <= totalDeposits + pubTotalDeposits.
    • finalizeSettlement(saleId)BOT, step 2; promotes pending → active only after waitingPeriod (min 6h).
    • revokeSettlementRoot(saleId) / setWaitingPeriod(seconds)MANAGER.
  • claim(saleId, account, refundAmount, shareToken, tokenAmount, proof) — permissionless, once per account, permanent. Verifies the leaf, pays the refund, then routes by the on-chain userTranche: unlocked → transfers tokenAmount of shareToken; locked → refund only, amount emitted for off-chain delivery.
  • initializeV2() (reinitializer(2)) — sets waitingPeriod = 6h; called at upgrade time so no post-upgrade config is needed.
  • New storage (appended over the subscription layout): Settlement struct (root/pendingRoot/pendingTotalRefund/totalRefund/lastSetTime/refunded), settlements, claimed, waitingPeriod.

Design decisions

  • One combined root, one claim per account — the leaf carries each account's two-round totals, so users claim once.
  • shareToken is in the leafleaf = keccak256(abi.encode(chainid, saleId, account, refundAmount, shareToken, tokenAmount)) — so no separate setShareToken step; the address is locked by the finalized root. Delivery mode is still routed by the on-chain userTranche (set at deposit).
  • Unlocked tranche requires shareToken != 0 — a zero share token is only valid for the locked tranche; otherwise claim reverts with a clear error instead of a cryptic transfer failure.
  • Refund accountingtotalRefund is the committed total (constant after finalize); refunded accumulates per claim, so outstanding = totalRefund - refunded is readable on-chain.
  • No withdrawProceeds — proceeds are withdrawn via the existing emergencyWithdraw (MANAGER), which already covers this; the settlement guard keeps a published root from over-committing refunds.
  • Roles — BOT drives the root lifecycle (automatable); MANAGER handles revoke/config/emergency; BOT's role-admin is MANAGER (granted at base init, so this upgrade needs no role setup).

Upgrade

UUPS; storage strictly appended over the subscription contract. Apply with upgradeToAndCall(newImpl, abi.encodeCall(PreIPODistributor.initializeV2, ())) so waitingPeriod is initialized atomically with the upgrade.

Testing

forge test --match-contract PreIPODistributorTest

@hashdit-bot

hashdit-bot Bot commented Jul 23, 2026

Copy link
Copy Markdown

Pull Request Review

This Solidity UUPS upgrade adds BOT-managed, time-delayed Merkle settlement roots and permissionless claims for refunds and share-token allocations. It also introduces settlement state, role administration, configurable review periods, and extensive Foundry tests for the new workflow.

Sensitive Content

No sensitive content detected.

Security Issues

🟠 [HIGH] Declared refund total is not enforced during claims

File: contracts/dao/PreIPODistributor.sol
setSettlementRoot limits the declared _totalRefund to the sale’s deposits, but claim never checks that s.refunded + _refundAmount <= s.totalRefund. Consequently, a malformed settlement tree can commit leaves whose aggregate refunds exceed both the declared total and the sale’s deposits. Because contract balances are pooled and additional roots can be finalized for the same sale while refunded remains cumulative, claims may consume funds belonging to other sales.
Recommendation: Before transferring a refund, require _refundAmount <= s.totalRefund - s.refunded, and ensure finalizing a replacement root cannot set totalRefund below amounts already refunded. If settlements are intended to be permanent, disallow setting another root after one has been finalized; otherwise define and enforce a cumulative replacement-root accounting model.


Generated by Hashdit Bot. This tool can absolutely NOT replace manual audits.

@razww
razww changed the base branch from feat/pre-ipo-distributor to master July 23, 2026 09:06
@razww
razww changed the base branch from master to feat/pre-ipo-distributor July 23, 2026 09:06
@razww
razww force-pushed the feat/pre-ipo-claim branch from 50b1dc9 to a6270a2 Compare July 23, 2026 09:32
@hashdit-bot

hashdit-bot Bot commented Jul 23, 2026

Copy link
Copy Markdown

Pull Request Review

This Solidity/UUPS upgrade adds a two-step, role-controlled settlement Merkle root lifecycle and permissionless claims for deposit refunds and share-token delivery. It appends settlement accounting storage, initializes a six-hour review period, and adds Foundry tests covering settlement, claims, roles, and initialization.

Sensitive Content

No sensitive content detected.

Security Issues

🟠 [HIGH] Claims are not bounded by the committed total refund

File: contracts/dao/PreIPODistributor.sol
setSettlementRoot validates only the declared _totalRefund, but claim never verifies that s.refunded + _refundAmount <= s.totalRefund. A malformed or malicious Merkle tree can therefore contain refund leaves whose aggregate exceeds the reviewed commitment, allowing valid claimants to drain deposit-token balances, including funds associated with other sales using the same token.
Recommendation: Before updating s.refunded, require _refundAmount <= s.totalRefund - s.refunded. Also test that claims whose cumulative refunds exceed totalRefund revert.

🟠 [HIGH] Finalized settlements can be replaced and refund commitments reused

File: contracts/dao/PreIPODistributor.sol
After a root is finalized, setSettlementRoot permits another root for the same sale and finalizeSettlement overwrites root and totalRefund without resetting or reconciling refunded. This violates the stated single-settlement design, permits multiple independently capped roots to pay different accounts from the same deposits, and can permanently prevent accounts that claimed under an earlier root from receiving corrected allocations because claimed is never versioned.
Recommendation: If settlement is intended to be permanent, reject setSettlementRoot once s.root is nonzero. If revisions are required, introduce explicit settlement versions, version the claimed/refund accounting, and enforce a cumulative refund liability that can never exceed the sale’s deposits.


Generated by Hashdit Bot. This tool can absolutely NOT replace manual audits.

@qingyang-lista qingyang-lista left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@qingyang-lista qingyang-lista left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@razww
razww force-pushed the feat/pre-ipo-claim branch from a6270a2 to d6fd6bd Compare July 23, 2026 09:55
@hashdit-bot

hashdit-bot Bot commented Jul 23, 2026

Copy link
Copy Markdown

Pull Request Review

This Solidity/UUPS upgrade adds a two-step, time-delayed Merkle settlement lifecycle and permissionless one-time claims for refunds and share-token allocations. It appends settlement storage, introduces initializeV2() and waiting-period management, and adds comprehensive Foundry tests for authorization, finalization, revocation, and claim behavior.

Sensitive Content

No sensitive content detected.

Security Issues

No serious security issues detected.


Generated by Hashdit Bot. This tool can absolutely NOT replace manual audits.

@razww
razww changed the base branch from feat/pre-ipo-distributor to master July 27, 2026 01:43
- M02: setPublicRound reverts once a settlement root is pending/finalized
- I02: remove redundant root inequality check in setSettlementRoot
- I03: pause now gates setSettlementRoot / finalizeSettlement / claim
- I04: document emergencyWithdraw accounting caveat (NatSpec)
- I09: cache duplicate deposit storage read in depositWhitelist
- I10/I14: complete claim param docs, header, tranche + permissionless-claim docs
- I13: add previewClaim read-only helper

L03 handled operationally; M01/L01/I01/I05-I08/I11/I12 acknowledged; L02 pending xKLSH address.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@razww razww mentioned this pull request Jul 28, 2026
@hashdit-bot

hashdit-bot Bot commented Jul 28, 2026

Copy link
Copy Markdown

Pull Request Review

This Solidity UUPS upgrade adds a two-step, time-delayed Merkle settlement lifecycle and permissionless, single-use claims for refunds and share-token delivery. It appends settlement storage, introduces initializeV2, adds public-round settlement guards and claim previews, and expands Foundry tests and audit documentation.

Sensitive Content

No sensitive content detected.

Security Issues

No serious security issues detected.


Generated by Hashdit Bot. This tool can absolutely NOT replace manual audits.

@qingyang-lista qingyang-lista left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@qingyang-lista
qingyang-lista merged commit b625c9c into master Jul 28, 2026
1 check passed
@qingyang-lista
qingyang-lista deleted the feat/pre-ipo-claim branch July 28, 2026 06:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants