Skip to content

[MerklPB] Migrate PoolBoosterFactoryMerkl to Beacon Proxy#2796

Open
clement-ux wants to merge 18 commits intomasterfrom
clement/improve-merklePB
Open

[MerklPB] Migrate PoolBoosterFactoryMerkl to Beacon Proxy#2796
clement-ux wants to merge 18 commits intomasterfrom
clement/improve-merklePB

Conversation

@clement-ux
Copy link
Collaborator

@clement-ux clement-ux commented Feb 12, 2026

Summary

Migrates PoolBoosterFactoryMerkl from deploying immutable PoolBoosterMerkl contracts to the OpenZeppelin Beacon Proxy pattern with a new upgradeable PoolBoosterMerklV2 implementation. Upgrading a single beacon automatically upgrades all existing Merkl pool boosters — no redeployment, no re-delegation.

Architecture

PoolBoosterFactoryMerkl (plain contract, cheaply redeployable)
    │
    ├── inherits AbstractPoolBoosterFactory (shared tracking logic)
    │
    ├── references → UpgradeableBeacon (OZ Ownable, owned by multichainStrategist)
    │                    │
    │                    └── points to → PoolBoosterMerklV2 implementation
    │
    └── deploys OZ BeaconProxy(beacon, initData) via CREATE2
         └── each proxy delegates to beacon.implementation()

Upgrading: multichainStrategist calls UpgradeableBeacon.upgradeTo(newImpl) → all existing pool boosters instantly use the new implementation.

Changes

New: PoolBoosterMerklV2.sol

  • Upgradeable pool booster behind BeaconProxy with Initializable + Strategizable
  • Configurable: duration, campaignType, rewardToken, merklDistributor, campaignData
  • bribe() restricted to factory / governor / strategist
  • rescueToken() restricted to governor
  • Uses acceptConditions() instead of isValidSignature (ERC-1271)
  • uint256[50] private __gap for upgrade safety
  • _setCampaignData validates length > 0
  • Implementation locked via constructor() initializer {}

Refactored: PoolBoosterFactoryMerkl.sol

  • Now inherits AbstractPoolBoosterFactory (shared pool booster tracking)
  • Deploys BeaconProxy via CREATE2 instead of directly deploying PoolBoosterMerkl instances
  • Factory receives raw initData (encoded initialize call) — no longer hardcodes constructor params
  • Constructor takes (oToken, governor, centralRegistry, beacon) — no longer stores merklDistributor
  • bribeAll() overridden with onlyGovernor
  • removePoolBooster() overridden to revert when address not found
  • computePoolBoosterAddress() predicts deterministic BeaconProxy address
  • Removed: merklDistributor storage, setMerklDistributor() (now per-booster)
  • Removed: duplicated tracking logic (uses inherited poolBoosters, poolBoosterFromPool, etc.)

Modified: AbstractPoolBoosterFactory.sol

  • bribeAll(): externalpublic virtual (overridable, callable from subclass)
  • removePoolBooster(): added virtual (overridable by factory)

Modified: IMerklDistributor.sol

  • Added acceptConditions() function signature

New: 175_deploy_pool_booster_merkl_factory.js

  1. Deploy PoolBoosterMerklV2 implementation
  2. Deploy UpgradeableBeacon pointing to it, transfer ownership to multichainStrategist
  3. Deploy PoolBoosterFactoryMerkl(oeth, multichainStrategist, registry, beacon)
  4. Governance proposal: remove old factory, approve new factory in central registry

Preserved: PoolBoosterMerkl.sol

  • Original V1 contract unchanged (still deployed on Sonic)

Test plan

  • All 53 mainnet fork tests passing
  • Beacon upgrade: deploy new impl → beacon.upgradeTo() → existing proxies use new logic
  • Implementation contract locked against initialize() calls
  • Duplicate AMM pool guard prevents double-creation
  • removePoolBooster reverts when address not found
  • Empty campaignData rejected on creation and via setCampaignData()
  • computePoolBoosterAddress returns correct deterministic address
  • bribeAll() respects exclusion list and executes bribes
  • All setter, bribe, rescue, and access control tests pass

🤖 Generated with Claude Code

- PoolBoosterMerkl: refactor to initializable pattern for clone compatibility
- PoolBoosterFactoryMerkl: use Clones.cloneDeterministic instead of CREATE2
- Add implementation, strategist storage with governor setters
- Simplify computePoolBoosterAddress to only require salt
clement-ux and others added 2 commits February 12, 2026 16:28
Accept raw `bytes calldata _initData` instead of typed initialize params,
making the factory implementation-agnostic. Remove merklDistributor and
strategist storage/setters as they are no longer needed at factory level.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Check implementation is set before validating other parameters.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@clement-ux clement-ux marked this pull request as draft February 12, 2026 15:30
clement-ux and others added 9 commits February 12, 2026 17:06
Set implementation in factory constructor and add deploy script that
swaps old factory for new in the central registry and creates an
initial Pool Booster.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… & factory

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove onlyGovernorOrStrategist from bribe() on PoolBoosterMerklV2 to
align with all other pool booster implementations (SwapxDouble, SwapxSingle,
Metropolis) and make bribeAll() on the factory work correctly. The previous
modifier caused bribeAll() to revert since msg.sender is the factory, not
the governor/strategist.

Test improvements:
- Use named constant MERKL_BOOSTER_TYPE instead of magic number 3
- Add comment explaining why init revert tests check "Initialization failed"
- Add withArgs check on TokensRescued event
- Add removePoolBooster auth test
- Add positive bribeAll test (executes bribes on funded boosters)
- Remove stale minAmount comment with wrong numbers
- Add note on createPoolBooster helper about || footgun

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…l() to governor

Store factory address during initialize() and require bribe() caller to be
factory, governor, or strategist. Override bribeAll() on PoolBoosterFactoryMerkl
with onlyGovernor. Add corresponding tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Drop the V2 suffix — no longer needed since the old contract is gone.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
bribe() now requires governor/strategist/factory caller.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…with old sonic contract

The old PoolBoosterMerkl (non-clone, constructor-based) is still used on sonic.
Renaming to PoolBoosterMerkl breaks the sonic factory compilation. Also fix sonic
test to use strategist for bribe() and PoolBoosterMerklV2 artifact.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@clement-ux clement-ux marked this pull request as ready for review February 12, 2026 21:56
Replace EIP-1167 minimal proxies (Clones) with OpenZeppelin BeaconProxy
so that upgrading a single GovernableBeacon automatically upgrades all
existing pool boosters without redeployment or re-delegation.

- Add GovernableBeacon: IBeacon + Origin Governable access control
- Refactor PoolBoosterFactoryMerkl: Initializable behind proxy, deploys
  BeaconProxy via CREATE2, inline pool booster tracking (no more
  AbstractPoolBoosterFactory inheritance)
- Add PoolBoosterFactoryMerklProxy in Proxies.sol
- Lock PoolBoosterMerklV2 implementation against direct initialization
- Add duplicate AMM pool guard, removePoolBooster revert on not found,
  campaignType validation, storage gap, remove unused oToken storage
- Update deployment script for beacon + factory proxy setup
- Update fixture and fork tests (53 passing)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@clement-ux clement-ux changed the title [MerklPB] Migrate PoolBoosterFactoryMerkl to Clones [MerklPB] Migrate PoolBoosterFactoryMerkl to Beacon Proxy Feb 13, 2026
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@clement-ux clement-ux force-pushed the clement/improve-merklePB branch from 83e3516 to 6872f16 Compare February 17, 2026 09:37
clement-ux and others added 4 commits February 17, 2026 11:20
Allow the strategist to upgrade the beacon implementation and manage
factory operations (create/remove pool boosters, bribeAll) without
going through governance. The governance proposal now only handles
registry operations (approveFactory/removeFactory).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The factory can be cheaply redeployed and swapped in the central
registry, so the proxy layer adds complexity without clear benefit.
Pool boosters themselves remain independently upgradeable via the beacon.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…, including migration to AbstractPoolBoosterFactory, addition of campaign data validation, and enhancements to initialization logic.
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.

1 participant