feat(dao): add MANAGER changeUserTranche correction (pre-finalize) - #121
Conversation
Adds changeUserTranche(saleId, account, tranche): a MANAGER-only override of an account's locked tranche, allowed until the settlement root is finalized (settlements.root == 0). A pending (not-yet-finalized) root does not block it — the pending root must then be revoked and the tree rebuilt off-chain to reflect the change. The new tranche must be valid, differ from the current one, and the account must already have a tranche (i.e. have deposited). Deposit amounts and aggregates are untouched — the tranche never affects on-chain accounting. Adds no storage (append-only layout unchanged; upgrade-safe). 8 tests: ok/acl/invalid-sale/invalid-tranche/no-deposit/same-tranche, allowed while a root is pending, and reverts after finalize. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Pull Request ReviewThis Solidity PR adds a MANAGER-only Sensitive ContentNo sensitive content detected. Security Issues🟠 [HIGH] Tranche changes can invalidate a pending settlement without restarting review
Generated by Hashdit Bot. This tool can absolutely NOT replace manual audits. |
Summary
Adds a MANAGER-only correction method,
changeUserTranche, toPreIPODistributor. It lets operations fix an account's locked delivery tranche after deposit (the tranche is otherwise locked at an account's first deposit), up until the settlement root is finalized. Stacks on the merged audit-fix work (#120); a single commit on top ofmaster.What it does
userTranche[saleId][account]and emitsChangeUserTranche(saleId, account, oldTranche, newTranche).deposits/pubDeposits) or the sale aggregates — the tranche never affects on-chain accounting, only off-chain delivery routing.Guards
onlyRole(MANAGER).whitelistRoot != 0).settlements[saleId].root == 0— allowed until the root is finalized. A pending (not-yet-finalized) root does not block the change; the operator must then revoke the pending root and rebuild the settlement tree off-chain so it reflects the corrected tranche.TRANCHE_UNLOCKED/TRANCHE_LOCKED).oldTranche != 0, i.e. has deposited)."Same tranche").Storage / upgrade safety
Adds no storage (one external function + one event). The storage layout is byte-identical to the current production contract, and remains an append-only, upgrade-safe superset of the live #118 subscription impl (
sales…nextSaleIdat slots 301–305 unchanged;settlements/claimed/waitingPeriodat 306–308). The sameupgradeToAndCall(newImpl, initializeV2())upgrade carries this change.Testing
forge test --match-contract PreIPODistributorTest— 64 passing, including 8 new cases:ok— manager flips XKLSH → PKLSH; deposits untouched, event emittedacl— non-MANAGER revertsinvalidSale/invalidTranche/noDeposit/sameTrancherevertswhileSettlementPending_ok— allowed while a root is pendingafterFinalize_reverts— reverts once finalized ("Settlement finalized")Operational note
Because a correction is permitted while a root is pending,
finalizeSettlementdoes not detect a tranche changed underneath a pending root — operations must revoke-and-rebuild before finalizing. Same class of operational discipline as L03.