Vanguard

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

Missing admin capabilities

Author Revealed upon completion

Description

  • The project’s README states that the owner “can modify fee parameters via administrative functions” and has “full administrative control over launch configuration.”

  • In the actual hook contract, all configuration fields - phase1Duration, phase2Duration, phase1LimitBps, phase2LimitBps, phase1Cooldown, phase2Cooldown, phase1PenaltyBps, phase2PenaltyBps—are declared immutable, set only in the constructor, and there are no setter functions or ownership/role checks to modify them post‑deployment. This contradicts the README’s stated capabilities and leaves the hook non‑configurable after deployment.

uint256 public immutable phase1Duration; // @> immutable; set once in constructor
uint256 public immutable phase2Duration; // @> immutable
uint256 public immutable phase1LimitBps; // @> immutable
uint256 public immutable phase2LimitBps; // @> immutable
uint256 public immutable phase1Cooldown; // @> immutable
uint256 public immutable phase2Cooldown; // @> immutable
uint256 public immutable phase1PenaltyBps; // @> immutable
uint256 public immutable phase2PenaltyBps; // @> immutable
// No onlyOwner / admin setters exist anywhere in the file. // @> no setters

Risk

Likelihood: Low

  • Always: Because the fields are immutable and there are no admin methods, no deployment of this hook can be reconfigured after construction. Any mention of runtime configurability in the README is therefore inaccurate.

Impact: Low

  • Operational rigidity / misconfiguration risk: If parameters are mis‑tuned (e.g., penalty too high, limit too low), the team cannot adjust them without redeploying a new hook and migrating pools/liquidity, which is disruptive and may be impractical mid‑launch. This contradicts what operators may rely on per the README.

Proof of Concept

  • The test below deploys the hook and then attempts to call several hypothetical admin setters via low‑level calls.

  • These calls fail.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
import {Test} from "forge-std/Test.sol";
import {Deployers} from "@uniswap/v4-core/test/utils/Deployers.sol";
import {Hooks} from "v4-core/libraries/Hooks.sol";
import {HookMiner} from "v4-periphery/src/utils/HookMiner.sol";
import {LPFeeLibrary} from "v4-core/libraries/LPFeeLibrary.sol";
import {IPoolManager} from "v4-core/interfaces/IPoolManager.sol";
import {TokenLaunchHook} from "../src/TokenLaunchHook.sol";
contract MissingAdminCapabilitiesTest is Test, Deployers {
TokenLaunchHook hook;
// Use the same parameter style as your suite
uint256 phase1Duration = 100;
uint256 phase2Duration = 200;
uint256 phase1LimitBps = 100;
uint256 phase2LimitBps = 300;
uint256 phase1Cooldown = 5;
uint256 phase2Cooldown = 3;
uint256 phase1PenaltyBps = 500;
uint256 phase2PenaltyBps = 200;
function setUp() public {
deployFreshManagerAndRouters();
// Mine correct flags: AFTER_INITIALIZE + BEFORE_SWAP
bytes memory creationCode = type(TokenLaunchHook).creationCode;
bytes memory args = abi.encode(
manager,
phase1Duration,
phase2Duration,
phase1LimitBps,
phase2LimitBps,
phase1Cooldown,
phase2Cooldown,
phase1PenaltyBps,
phase2PenaltyBps
);
uint160 flags = uint160(Hooks.BEFORE_SWAP_FLAG | Hooks.AFTER_INITIALIZE_FLAG);
(address mined, bytes32 salt) = HookMiner.find(address(this), flags, creationCode, args);
hook = new TokenLaunchHook{salt: salt}(
IPoolManager(manager),
phase1Duration,
phase2Duration,
phase1LimitBps,
phase2LimitBps,
phase1Cooldown,
phase2Cooldown,
phase1PenaltyBps,
phase2PenaltyBps
);
require(address(hook) == mined, "Hook address mismatch");
}
function test_NoAdminSetters_ImmutablesRigid() public {
// Snapshot current (constructor) values
uint256 d1 = hook.phase1Duration();
uint256 d2 = hook.phase2Duration();
uint256 l1 = hook.phase1LimitBps();
uint256 l2 = hook.phase2LimitBps();
uint256 c1 = hook.phase1Cooldown();
uint256 c2 = hook.phase2Cooldown();
uint256 p1 = hook.phase1PenaltyBps();
uint256 p2 = hook.phase2PenaltyBps();
// Attempt to call hypothetical admin setters via low-level calls
// (these functions do not exist; calls should return false)
(bool ok1, ) = address(hook).call(abi.encodeWithSignature("setPhase1Duration(uint256)", 999));
(bool ok2, ) = address(hook).call(abi.encodeWithSignature("setPhase2Duration(uint256)", 999));
(bool ok3, ) = address(hook).call(abi.encodeWithSignature("setPhase1LimitBps(uint256)", 9999));
(bool ok4, ) = address(hook).call(abi.encodeWithSignature("setPhase2LimitBps(uint256)", 9999));
(bool ok5, ) = address(hook).call(abi.encodeWithSignature("setPhase1Cooldown(uint256)", 0));
(bool ok6, ) = address(hook).call(abi.encodeWithSignature("setPhase2Cooldown(uint256)", 0));
(bool ok7, ) = address(hook).call(abi.encodeWithSignature("setPhase1PenaltyBps(uint256)", 0));
(bool ok8, ) = address(hook).call(abi.encodeWithSignature("setPhase2PenaltyBps(uint256)", 0));
assertFalse(ok1 || ok2 || ok3 || ok4 || ok5 || ok6 || ok7 || ok8, "unexpected setter(s) present");
// Verify nothing changed (immutables locked to constructor values)
assertEq(hook.phase1Duration(), d1, "phase1Duration changed unexpectedly");
assertEq(hook.phase2Duration(), d2, "phase2Duration changed unexpectedly");
assertEq(hook.phase1LimitBps(), l1, "phase1LimitBps changed unexpectedly");
assertEq(hook.phase2LimitBps(), l2, "phase2LimitBps changed unexpectedly");
assertEq(hook.phase1Cooldown(), c1, "phase1Cooldown changed unexpectedly");
assertEq(hook.phase2Cooldown(), c2, "phase2Cooldown changed unexpectedly");
assertEq(hook.phase1PenaltyBps(), p1, "phase1PenaltyBps changed unexpectedly");
assertEq(hook.phase2PenaltyBps(), p2, "phase2PenaltyBps changed unexpectedly");
}
}

Recommended Mitigation

  • Remove the immutable qualifiers; store the parameters as standard uint256 state.

  • Add access control (e.g., Ownable or AccessControl) and setter functions guarded by onlyOwner.

  • Emit events on parameter updates; validate bounds (≤ 10_000 bps, non‑zero durations, etc.).

- uint256 public immutable phase1Duration;
+ uint256 public phase1Duration;
- uint256 public immutable phase1LimitBps;
+ uint256 public phase1LimitBps;
// ...repeat for all eight parameters
+ // Example access control (choose what your stack uses)
+ import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
+ contract TokenLaunchHook is BaseHook, Ownable {
+ constructor(/*...*/) BaseHook(_poolManager) Ownable(msg.sender) { /* initialize params */ }
+
+ event ParamsUpdated(uint256 phase1Duration, uint256 phase2Duration, /* ... */);
+
+ function setPhase1Duration(uint256 v) external onlyOwner {
+ require(v > 0, "phase1Duration=0");
+ phase1Duration = v;
+ emit ParamsUpdated(phase1Duration, phase2Duration, /* ... */);
+ }
+ // Add similar setters with validation for each parameter
+ }

Support

FAQs

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

Give us feedback!