Skip to content

feat(dao): add topUpEpoch to remediate under-funded launchpool epochs - #123

Merged
razww merged 3 commits into
masterfrom
feat/launchpool-topup-epoch
Aug 17, 2026
Merged

feat(dao): add topUpEpoch to remediate under-funded launchpool epochs#123
razww merged 3 commits into
masterfrom
feat/launchpool-topup-epoch

Conversation

@qa-august-l

Copy link
Copy Markdown
Contributor

Summary

Adds topUpEpoch to ClisBNBLaunchPoolDistributor to remediate launchpool epochs that were configured with a total reward smaller than the sum of their merkle-tree leaf amounts, which leaves tail users unable to claim.

Problem & root cause

Several pendle-v3 launchpool epochs were set up with epoch.totalAmount < Σ(leaf amounts):

  • setEpochMerkleRoot() stores the operator-supplied _totalAmount as both epoch.totalAmount and epoch.unclaimedAmount and adds it to totalUnclaimedAmount[token] (ClisBNBLaunchPoolDistributor.sol:131-133). Nothing on-chain ties _totalAmount to the leaf sum (the full tree isn't available on-chain).
  • claim() does epoch.unclaimedAmount -= _amount (:101). Once cumulative claims exceed the under-set cap, this underflows (Solidity 0.8 checked math) and every remaining claim reverts. First-come-first-served, so the tail users are permanently blocked.
  • There was no fix path for an already-started epoch: setEpochMerkleRoot only accepts _epochId == nextEpochId (:119) and revokeEpoch requires startTime > block.timestamp (:144).

The merkle root is already correct — it contains every user, and Σ leaves is the correct total. Only the totalAmount accounting cap was set too low.

Fix

topUpEpoch(uint64 _epochId, uint256 _newTotalAmount) raises an existing epoch's totalAmount / unclaimedAmount / totalUnclaimedAmount[token] to the correct sum-of-leaves total. The merkle root is unchanged, so already-claimed users are unaffected and remaining users' proofs stay valid; raising unclaimedAmount removes the underflow so the tail can claim.

  • Access control: DEFAULT_ADMIN_ROLE (same as collectUnclaimed / adminTransfer) — raising fund accounting is a privileged corrective action.
  • Pass the correct total, not a delta: callers pass Σ leaves; strictly-increasing + idempotent-safe (re-applying reverts on "Not an increase").
  • Guards: epoch must exist (Invalid epochId), still be within its claim window (Epoch ended — a topped-up ended epoch would only strand accounting since claim stays inactive), amount must strictly increase (Not an increase), and a solvency check balanceOf(token) >= totalUnclaimedAmount[token] (Insufficient funds) forces funding the delta before raising the cap (native BNB via address(this).balance, ERC20 via balanceOf).
  • Upgrade-safe: no new storage variables — storage layout is unchanged (verified via forge inspect storage-layout).

Testing

  • New unit tests (test/dao/ClisBNBLaunchPoolDistributorUnit.t.sol, non-fork, mock ERC20): tail-claim goes revert -> success after top-up, plus access control (non-admin rejected), only-increase, solvency (ERC20 + native BNB), invalid-epoch, ended-epoch, and accounting/event coverage. 8/8 pass.
  • Existing fork suite (ClisBNBLaunchPoolDistributor.t.sol): 14/14 pass, no regression (change is purely additive).
  • End-to-end BSC fork against live epoch 123 (pendle-v3-CHIP, BNB): 247 leaves, Σ = 2.520985730 BNB vs on-chain totalAmount 2.246244. Before: 31 tail users blocked with the accounting underflow, totaling 0.274742722 BNB (exactly the shortfall). After upgrading the impl + topUpEpoch(123, Σ): all 31 become claimable, 0 blocked, epoch budget lands exactly at 0.

Rollout note

The distributor is a shared pool that already holds enough BNB (the block is purely per-epoch accounting, not a token shortage), so remediation for each under-set epoch is: deploy the new impl → upgrade the proxy → topUpEpoch(epochId, Σleaves_wei). The solvency guard verifies the contract holds enough before raising the cap.

🤖 Generated with Claude Code

Some pendle-v3 launchpool epochs were configured with totalAmount less than
the sum of the merkle-tree leaf amounts. Once cumulative claims exceed that
under-set cap, `claim` underflows `epoch.unclaimedAmount` (Solidity 0.8
checked math) and tail users can no longer claim. Already-started epochs had
no fix path: setEpochMerkleRoot is new-epoch-only and revokeEpoch is
pre-start-only.

topUpEpoch raises an existing epoch's totalAmount / unclaimedAmount /
totalUnclaimedAmount to the correct sum-of-leaves total. The merkle root is
unchanged — it already contains every user, only the accounting caps were
wrong. Gated on DEFAULT_ADMIN_ROLE (like collectUnclaimed / adminTransfer),
since raising fund accounting is a privileged corrective action. Guards:
epoch must exist and still be within its claim window (else the top-up would
strand accounting while claim stays inactive), amount must strictly increase,
and a solvency check (balance >= totalUnclaimedAmount) forces funding the
delta before raising the cap. No new storage variables, so the upgrade is
storage-layout compatible.

Tested with non-fork unit tests (mock ERC20): tail-claim goes revert ->
success after top-up, plus access control (non-admin rejected), only-increase,
solvency (ERC20 and native), invalid-epoch, ended-epoch, accounting and event
coverage. Verified end-to-end on a BSC fork against live epoch 123: 31 blocked
users (0.274742722 BNB) all become claimable after upgrade + topUpEpoch.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@qa-august-l
qa-august-l force-pushed the feat/launchpool-topup-epoch branch from 833fcb2 to 4ffd42b Compare August 11, 2026 05:52

@razww razww left a comment

Copy link
Copy Markdown
Collaborator

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

HashDit audit [M02]: topUpEpoch only bounded the upper edge of the claim
window (block.timestamp <= endTime), which a pre-start epoch always satisfies.
Topping one up pulls its funding forward into the shared
totalUnclaimedAmount[token] -- possibly blocking a legitimate top-up of an
active sibling epoch -- and since revokeEpoch adjusts accounting without
returning tokens, top-up-then-revoke leaves an unearmarked surplus behind.
That surplus is exactly what lets a later top-up pass the token-wide solvency
check without the delta being funded ([M01]). A pre-start epoch should be
corrected via revokeEpoch + recreate instead.

Adds `require(block.timestamp >= epoch.startTime, "Epoch not started")` so the
window is bounded on both edges. Ended epochs still revert with "Epoch ended".

Tests: two M02 cases (pre-start rejection with accounting untouched; the
top-up-then-revoke surplus route stays closed) and four multi-epoch cases on a
shared token, which the previous suite did not cover -- per-epoch isolation of
the caps, end-to-end tail-claim remediation alongside an untouched sibling, and
both directions of the aggregate solvency check ([M01], accepted): a sibling
surplus satisfying an unfunded delta, and a sibling deficit blocking a funded
top-up. The last two assert current behaviour deliberately, so moving to
per-epoch funding later fails them instead of drifting silently.

Also trims the topUpEpoch NatSpec to the @dev summary.

28 tests pass across both distributor suites.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@hashdit-bot

hashdit-bot Bot commented Aug 13, 2026

Copy link
Copy Markdown

Pull Request Review

This Solidity PR adds an admin-gated topUpEpoch function to increase reward accounting for active launchpool epochs whose configured totals were below their Merkle leaf sums. It includes aggregate solvency checks for ERC-20 and native BNB rewards, emits a new event, and adds unit tests covering claims, authorization, accounting, lifecycle guards, and multi-epoch 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.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@hashdit-bot

hashdit-bot Bot commented Aug 17, 2026

Copy link
Copy Markdown

Pull Request Review

This Solidity PR adds an admin-gated topUpEpoch function to increase the accounting budget of active, under-funded launchpool epochs while preserving their Merkle roots. It includes aggregate token solvency and claim-window checks, emits a new event, and adds unit tests covering claims, authorization, accounting, native/ERC-20 funding, and multi-epoch 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 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM

@razww
razww merged commit 28a3c02 into master Aug 17, 2026
1 check passed
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.

4 participants