Vanguard

First Flight #56
Beginner FriendlyDeFiFoundry
0 EXP
Submission Details
Impact: medium
Likelihood: high

Phase parameters declared as `immutable` prevent modification and per-pool configuration, contradicting documented functionality in protocol README

Author Revealed upon completion

Phase parameters declared as immutable prevent modification and per-pool configuration, contradicting documented functionality in protocol README

Description

All phase configuration parameters are declared as immutable:

uint256 public immutable phase1Duration;
uint256 public immutable phase2Duration;
uint256 public immutable phase1LimitBps;
uint256 public immutable phase2LimitBps;
uint256 public immutable phase1Cooldown;
uint256 public immutable phase2Cooldown;
uint256 public immutable phase1PenaltyBps;
uint256 public immutable phase2PenaltyBps;

This causes two distinct problems:

1. Parameters cannot be modified post-deployment

The README explicitly states the owner "can modify fee parameters via administrative functions" and "has full administrative control over launch configuration". However, immutable variables are set once in the constructor and cannot be changed afterward - this is enforced at the Solidity compiler level.

2. Parameters cannot be per-pool

As documented in a separate finding, the hook's state should be keyed by PoolId to support multiple pools. However, immutable variables cannot be mappings - they must be value types. This means:

  • All pools using this hook share identical configuration

  • Different token launches cannot have customized durations/limits/penalties

  • A hook deployment can only serve one "type" of launch

Risk

Likelihood:

  • This is a permanent architectural limitation from deployment

  • Every pool initialized with this hook inherits the same immutable configuration

Impact:

  • The specified administrative functions would not work even if they were implemented

  • Cannot support multiple token launches with different desired launch configurations

Proof of Concept

Solidity immutable variables are stored in the contract bytecode, not in storage. They are set during construction and cannot be modified:

// This would fail to compile - immutables cannot have setters
function setPhase1Duration(uint256 _duration) external onlyOwner {
phase1Duration = _duration; // Error: Cannot assign to immutable variable
}

Recommended Mitigation

There are two valid approaches depending on the desired trust model:

Option A: Global Config with Setters

Keep configuration global but make it mutable. This matches the documented behavior where "Owner has unrestricted access to modify parameters".

Pros:

  • Simpler implementation

  • No coordination needed for new pools

  • Owner can respond to bot activity mid-launch (feature per README)

Cons:

  • All pools share the same config

  • Changes affect existing pools immediately (accepted centralization risk per README)

- uint256 public immutable phase1Duration;
- uint256 public immutable phase2Duration;
- uint256 public immutable phase1LimitBps;
- uint256 public immutable phase2LimitBps;
- uint256 public immutable phase1Cooldown;
- uint256 public immutable phase2Cooldown;
- uint256 public immutable phase1PenaltyBps;
- uint256 public immutable phase2PenaltyBps;
+ uint256 public phase1Duration;
+ uint256 public phase2Duration;
+ uint256 public phase1LimitBps;
+ uint256 public phase2LimitBps;
+ uint256 public phase1Cooldown;
+ uint256 public phase2Cooldown;
+ uint256 public phase1PenaltyBps;
+ uint256 public phase2PenaltyBps;
+ function setPhaseConfig(
+ uint256 _phase1Duration,
+ uint256 _phase2Duration,
+ uint256 _phase1LimitBps,
+ uint256 _phase2LimitBps,
+ uint256 _phase1Cooldown,
+ uint256 _phase2Cooldown,
+ uint256 _phase1PenaltyBps,
+ uint256 _phase2PenaltyBps
+ ) external onlyOwner {
+ if (_phase1Duration == 0 || _phase2Duration == 0) revert InvalidConfig();
+ if (_phase1LimitBps > 10000 || _phase2LimitBps > 10000) revert InvalidConfig();
+ if (_phase1PenaltyBps > 10000 || _phase2PenaltyBps > 10000) revert InvalidConfig();
+
+ phase1Duration = _phase1Duration;
+ phase2Duration = _phase2Duration;
+ phase1LimitBps = _phase1LimitBps;
+ phase2LimitBps = _phase2LimitBps;
+ phase1Cooldown = _phase1Cooldown;
+ phase2Cooldown = _phase2Cooldown;
+ phase1PenaltyBps = _phase1PenaltyBps;
+ phase2PenaltyBps = _phase2PenaltyBps;
+ }

Option B: Per-Pool Config (More Isolation)

Each pool gets its own configuration, set by owner at pool creation time.

Pros:

  • Different token launches can have different parameters

  • Pools are isolated from config changes to other pools

  • More granular control

Cons:

  • Requires owner coordination for each new pool

  • More complex operationally

  • Owner must be notified when pools are created

- uint256 public immutable phase1Duration;
- uint256 public immutable phase2Duration;
- uint256 public immutable phase1LimitBps;
- uint256 public immutable phase2LimitBps;
- uint256 public immutable phase1Cooldown;
- uint256 public immutable phase2Cooldown;
- uint256 public immutable phase1PenaltyBps;
- uint256 public immutable phase2PenaltyBps;
+ struct PhaseConfig {
+ uint256 phase1Duration;
+ uint256 phase2Duration;
+ uint256 phase1LimitBps;
+ uint256 phase2LimitBps;
+ uint256 phase1Cooldown;
+ uint256 phase2Cooldown;
+ uint256 phase1PenaltyBps;
+ uint256 phase2PenaltyBps;
+ }
+
+ mapping(PoolId => PhaseConfig) public poolConfig;
+ function setPoolConfig(
+ PoolId poolId,
+ PhaseConfig calldata config
+ ) external onlyOwner {
+ if (config.phase1Duration == 0 || config.phase2Duration == 0) revert InvalidConfig();
+ if (config.phase1LimitBps > 10000 || config.phase2LimitBps > 10000) revert InvalidConfig();
+ if (config.phase1PenaltyBps > 10000 || config.phase2PenaltyBps > 10000) revert InvalidConfig();
+
+ poolConfig[poolId] = config;
+ }

Then update _beforeSwap to read from poolConfig[key.toId()] instead of the immutable variables.

Note: Regardless of which option is chosen, the runtime state (currentPhase, initialLiquidity, tracking mappings) must still be per-pool as described in the "Shared state across all pools" finding - that is a separate architectural issue.

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!