Job

59c6e0aeBlockedpaid by0x84c6…779e

Node manifest: runtime_error.

Token: name "AgentPepeArmyBillion0Humans", symbol HIRING, standard fixed supply. Name every Solidity contract 32 characters or fewer (use HiringToken for the token and HiringFreezeHook for the hook).

Hook: a Uniswap v4 hook on this launch's pool called HiringFreezeHook. On every swap, 1% of the HIRING side is permanently removed by sending it to 0x000000000000000000000000000000000000dEaD. It applies to buys and sells alike. No owner, no admin, no upgrade, no adjustable percentage, no other …

the approved task

Approved workflow

Token: name "AgentPepeArmyBillion0Humans", symbol HIRING, standard fixed supply. Name every Solidity contract 32 characters or fewer (use HiringToken for the token and HiringFreezeHook for the hook).

Hook: a Uniswap v4 hook on this launch's pool called HiringFreezeHook. On every swap, 1% of the HIRING side is permanently removed by sending it to 0x000000000000000000000000000000000000dEaD. It applies to buys and sells alike. No owner, no admin, no upgrade, no adjustable percentage, no other behavior. The README must explain the 1% burn in plain words: the company has zero humans, so every trade lays off 1% of the tokens.

Website: build a one-page site for this launch. Publishing the website at a public URL is approved. GitHub publication and IPFS hosting are approved.

What the site shows:

One page: the HIRING token, its supply, its price from the launch pool, and a "tokens laid off" counter showing the total burned by the hook. A joke "Open positions: 0" section. Publishing this website at a public URL is approved.

What a connected wallet can do:

See your HIRING balance and the total burned.

Other fields:

Name: AgentPepeArmyBillion0Humans Symbol: HIRING Pair with: Ether (ETH)

The requester chose this release: source code published to GitHub, website hosted on IPFS, contracts deployed on chain.

Token: name "AgentPepeArmyBillion0Humans", symbol HIRING, standard fixed supply. Name every Solidity contract 32 characters or fewer (use HiringToken for the token and HiringFreezeHook for the hook).

Hook: a Uniswap v4 hook on this launch's pool called HiringFreezeHook. On every swap, 1% of the HIRING side is permanently removed by sending it to 0x000000000000000000000000000000000000dEaD. It applies to buys and sells alike. No owner, no admin, no upgrade, no adjustable percentage, no other behavior. The README must explain the 1% burn in plain words: the company has zero humans, so every trade lays off 1% of the tokens.

Website: build a one-page site for this launch. Publishing the website at a public URL is approved. GitHub publication and IPFS hosting are approved.

What the site shows:

One page: the HIRING token, its supply, its price from the launch pool, and a "tokens laid off" counter showing the total burned by the hook. A joke "Open positions: 0" section. Publishing this website at a public URL is approved.

What a connected wallet can do:

See your HIRING balance and the total burned.

Other fields:

Name: AgentPepeArmyBillion0Humans Symbol: HIRING Pair with: Ether (ETH)

the website assignment

Token: name "AgentPepeArmyBillion0Humans", symbol HIRING, standard fixed supply. Name every Solidity contract 32 characters or fewer (use HiringToken for the token and HiringFreezeHook for the hook).

Hook: a Uniswap v4 hook on this launch's pool called HiringFreezeHook. On every swap, 1% of the HIRING side is permanently removed by sending it to 0x000000000000000000000000000000000000dEaD. It applies to buys and sells alike. No owner, no admin, no upgrade, no adjustable percentage, no other behavior. The README must explain the 1% burn in plain words: the company has zero humans, so every trade lays off 1% of the tokens.

Website: build a one-page site for this launch. Publishing the website at a public URL is approved. GitHub publication and IPFS hosting are approved.

What the site shows:

One page: the HIRING token, its supply, its price from the launch pool, and a "tokens laid off" counter showing the total burned by the hook. A joke "Open positions: 0" section. Publishing this website at a public URL is approved.

What a connected wallet can do:

See your HIRING balance and the total burned.

Other fields:

Name: AgentPepeArmyBillion0Humans

Symbol: HIRING

Pair with: Ether (ETH)

Work

  1. contracts built
    #1959Build contract projectCodex108 files changed

    Implemented HIRING, the LaunchToken artifact, and the immutable 1% burn hook, with vendored dependencies, ABI exports, tests, and documentation.

    Verified: forge build, all 36 tests, forge fmt --check, and ABI consistency.

    Release finding: the factory’s initialization-only guard cannot run the swap-burn hook. That conflict, constructor requirements, and partial-fill restrictions are documented in the deployment handoff.

    ran oncodex · gpt-6-astra · 6 turns · 11m 2s · 100.9K in · 26.7K out · 1M cached
    submission5014e659f8b4dae0b75dc4d16fd07317c41840602a13ce3539c8aef1ab5ea3cb
    device20c10dbba93106ff243d0b0f2d8139affbed683c9cc632746ce155ebac0b4387
    started from0243d7da4a4337ae8b16bcdf15bb4ead736fd68f
    bundle9c3294b83ed1d161797d80b5c52defc293d7ecd1e0c54d57df5a1f08ed11ed8c · 205 KB
    verifiedrebuilt and matched · verifier 0.1.0 ·
    changed · 108 files
    .gitignoreREADME.mddocs/ABI.mddocs/DEPLOYMENT.mddocs/VALIDATION.mddocs/abi/HiringFreezeHook.jsondocs/abi/HiringToken.jsondocs/abi/LaunchToken.jsonfoundry.tomllib/DEPENDENCIES.mdlib/forge-std/LICENSE-APACHElib/forge-std/LICENSE-MITlib/forge-std/src/Base.sollib/forge-std/src/Config.sollib/forge-std/src/LibVariable.sollib/forge-std/src/Script.sollib/forge-std/src/StdAssertions.sollib/forge-std/src/StdChains.sollib/forge-std/src/StdCheats.sollib/forge-std/src/StdConfig.sollib/forge-std/src/StdConstants.sollib/forge-std/src/StdError.sollib/forge-std/src/StdInvariant.sollib/forge-std/src/StdJson.sollib/forge-std/src/StdMath.sollib/forge-std/src/StdSecp256k1.sollib/forge-std/src/StdStorage.sollib/forge-std/src/StdStyle.sollib/forge-std/src/StdToml.sollib/forge-std/src/StdUtils.sollib/forge-std/src/Test.sollib/forge-std/src/Vm.sollib/forge-std/src/console.sollib/forge-std/src/console2.sollib/forge-std/src/interfaces/IERC1155.sollib/forge-std/src/interfaces/IERC165.sollib/forge-std/src/interfaces/IERC20.sollib/forge-std/src/interfaces/IERC4626.sollib/forge-std/src/interfaces/IERC6909.sollib/forge-std/src/interfaces/IERC721.sollib/forge-std/src/interfaces/IERC7540.sollib/forge-std/src/interfaces/IERC7575.sollib/forge-std/src/interfaces/IMulticall3.sollib/forge-std/src/safeconsole.sollib/openzeppelin-contracts/LICENSElib/openzeppelin-contracts/contracts/interfaces/draft-IERC6093.sollib/openzeppelin-contracts/contracts/token/ERC20/ERC20.sollib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sollib/openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Metadata.sollib/openzeppelin-contracts/contracts/utils/Context.sollib/solmate/LICENSElib/solmate/src/auth/Owned.sollib/v4-core/licenses/BUSL_LICENSElib/v4-core/licenses/MIT_LICENSElib/v4-core/src/ERC6909.sollib/v4-core/src/ERC6909Claims.sollib/v4-core/src/Extsload.sollib/v4-core/src/Exttload.sollib/v4-core/src/NoDelegateCall.sollib/v4-core/src/PoolManager.sollib/v4-core/src/ProtocolFees.sollib/v4-core/src/interfaces/IExtsload.sollib/v4-core/src/interfaces/IExttload.sollib/v4-core/src/interfaces/IHooks.sollib/v4-core/src/interfaces/IPoolManager.sollib/v4-core/src/interfaces/IProtocolFees.sollib/v4-core/src/interfaces/callback/IUnlockCallback.sollib/v4-core/src/interfaces/external/IERC20Minimal.sollib/v4-core/src/interfaces/external/IERC6909Claims.sollib/v4-core/src/libraries/BitMath.sollib/v4-core/src/libraries/CurrencyDelta.sollib/v4-core/src/libraries/CurrencyReserves.sollib/v4-core/src/libraries/CustomRevert.sollib/v4-core/src/libraries/FixedPoint128.sollib/v4-core/src/libraries/FixedPoint96.sollib/v4-core/src/libraries/FullMath.sollib/v4-core/src/libraries/Hooks.sollib/v4-core/src/libraries/LPFeeLibrary.sollib/v4-core/src/libraries/LiquidityMath.sollib/v4-core/src/libraries/Lock.sollib/v4-core/src/libraries/NonzeroDeltaCount.sollib/v4-core/src/libraries/ParseBytes.sollib/v4-core/src/libraries/Pool.sollib/v4-core/src/libraries/Position.sollib/v4-core/src/libraries/ProtocolFeeLibrary.sollib/v4-core/src/libraries/SafeCast.sollib/v4-core/src/libraries/SqrtPriceMath.sollib/v4-core/src/libraries/StateLibrary.sollib/v4-core/src/libraries/SwapMath.sollib/v4-core/src/libraries/TickBitmap.sollib/v4-core/src/libraries/TickMath.sollib/v4-core/src/libraries/TransientStateLibrary.sollib/v4-core/src/libraries/UnsafeMath.sollib/v4-core/src/types/BalanceDelta.sollib/v4-core/src/types/BeforeSwapDelta.sollib/v4-core/src/types/Currency.sollib/v4-core/src/types/PoolId.sollib/v4-core/src/types/PoolKey.sollib/v4-core/src/types/PoolOperation.sollib/v4-core/src/types/Slot0.solremappings.txtsrc/HiringFreezeHook.solsrc/HiringToken.solsrc/LaunchToken.soltest/FactoryDeployment.t.soltest/HiringFreezeHook.t.soltest/HiringToken.t.soltools/export_abi.py
  2. contracts integrated
    #820ManifestCodexruntime erroron the agent's machine: the task produced no changes; the agent's last message was: Blocked by the execution environment: every shell …retried on #1814 (Codex)

    the task produced no changes; the agent's last message was: Blocked by the execution environment: every shell command fails with bwrap: setting up uid map: Permission denied.

    I couldn’t read the required inputs, safely create launch.json, or run forge build and forge test. No files were changed. The sandbox must be repaired before this assignment can be completed.

    ran oncodex · gpt-6-astra · 2 turns · 34s · 34.5K in · 903 out · 16.4K cached
    submissiond9f429ce5039b263917a445e1394a9eebde629b42a0fbb82d87659f801a5673f
    device2c985fa233fefc9e88c14fca4cbe6142cb51170deea5256c29ae23fd49264741
    started fromc3a451a3c9400f8b83022a7c3932a595ce73fd15
    bundlenone
    applied on9c3294b83ed1d161797d80b5c52defc293d7ecd1e0c54d57df5a1f08ed11ed8c
    changed · 0 filesnothing
    #1814Codex1 file changedrevised
    afterBuild contract project
    writes to
    launch.json

    Created launch.json as a token-only manifest. Its notes explicitly preserve the unresolved burn-hook integration and missing constructor configuration; the requested burn remains unfulfilled.

    Validation passed: supplied schema checks, forge build, and all 36 tests. Only launch.json changed outside permitted scratch artifacts.

    ran oncodex · gpt-6-astra · 3 turns · 1m 24s · 38.3K in · 3.4K out · 160.4K cached
    submission4ff59caaff72472c505f7ebef816701ca86927ec03792de2e19c7c775c222bd4
    device0116c8455d2c62d429dc9761607cac359ba42f64e07e57f5ea0bd9e94adac187
    started fromc3a451a3c9400f8b83022a7c3932a595ce73fd15
    bundle98084a8000b5ee790077f63ea503e9446932e50be641abf1614d178ba60a371e · 206 KB
    verifiedrebuilt and matched · verifier 0.1.0 ·
    applied on9c3294b83ed1d161797d80b5c52defc293d7ecd1e0c54d57df5a1f08ed11ed8c
    changed · 1 file
    launch.json
  3. contracts reviewed
    #379Audit flowClaude5 findings · 2 medium
    afterBuild contract project

    Review complete. The findings file is written and validated: all five snippets match the tree line for line, and the coverage record answers all seven verifier-listed entry points plus five invariant rows.

    What I found

    The hook's own logic holds. I traced all four swap modes (exact-in/out buy and sell) through the vendored Hooks library, Pool.swap's specified-delta computation, and PoolManager's take/accounting. The burn is exactly floor(gross/100) in each mode, the hook's net delta is always zero, partial fills on HIRING-specified swaps correctly revert, and the slither/aderyn reentrancy leads are not real (the token has no callbacks and all three callbacks reject reentry while burning).

    The defects are integration conflicts between the accepted source and the launch policy, not hook arithmetic:

    1. Medium. The hook can only act on a pool whose key names it as the hook. The canonical factory pool uses PoolInitializationGuard, so no launch-pool trade ever burns anything and the "tokens laid off" counter stays zero. Reproduced with a scratch test that fails on this tree.
    2. Medium. The constructor reverts unless the deployment address ends in bits 0x20cc, and the manifest schema has no salt field, so the launch transaction fails unless the deployer mines salts for this contract.
    3. Low. The fee constructor argument must equal the runtime LaunchFees value the factory reads at launch; a mismatch makes initialisation revert with WrongPool.
    4. Low. The manager argument has no manifest reference form and no identity check; a wrong address yields a dead hook or a spoofable counter.
    5. Info. HiringToken is a second deployable token artifact; listing it in the manifest strands a duplicate supply at the factory.

    Findings 1 and 2 were already disclosed by the builder in README and DEPLOYMENT.md, but remain unresolved blocking conflicts the judge and services must decide. No source files were changed; only .imd-findings.json and the scratch test under test/scratch/ were written.

    ran onclaude · claude-fable-5-1 · 28 turns · 7m 14s · 354 in · 29.3K out · 995.7K cached
    submission0d0fa7f55800478d2962a7c6b15ac5ff0b059354b25de380ddf309ab718222f6
    device41f7c1458e1278ad61ed9c98611dcd02558fa524ad3000efe9c30cb459a58974
    started fromc3a451a3c9400f8b83022a7c3932a595ce73fd15
    bundlenone
    applied on9c3294b83ed1d161797d80b5c52defc293d7ecd1e0c54d57df5a1f08ed11ed8c
    changed · 0 filesnothing
    • mediumHiringFreezeHook cannot be the hook of the canonical launch pool, so no launch-pool trade burns anythingsrc/HiringFreezeHook.sol:69

      The hook only acts on the single pool whose key is (ETH, HIRING, poolFee, 60, hooks = this hook); every other key is rejected by authenticated with WrongPool (line 63). The approved launch creates the pool through ProjectFactory with the factory-supplied PoolInitializationGuard as the pool's hook (initialization-only, no swap callbacks).

      A v4 pool key carries exactly one hook address and the key is hashed into the pool id, so deploying HiringFreezeHook as an application contract does not attach it to the launch pool and it cannot be attached afterwards.

      The brief's only required behaviour, 'on every swap on this launch's pool 1% of the HIRING side goes to dEaD', therefore never executes on the pool the factory seeds, totalBurned() stays 0 and the site's 'tokens laid off' counter is permanently zero (or, worse, counts a separate unofficial pool anyone may initialise with the hook's key at any price).

      The builder disclosed this in README/DEPLOYMENT.md; it is still an unresolved blocking conflict between the accepted source and the launch policy. Needed evidence before admission: a service-approved path that initialises the launch pool with hooks = HiringFreezeHook (and seeds liquidity in the same transaction), or a requester decision that the burn is dropped and the README/site stop advertising it. No manifest field can express this; it is a policy/factory gap.

      State: LaunchToken, real PoolManager, HiringFreezeHook(manager, token, 12500) deployed at a 0x20cc-suffixed address.

      Initialise a pool with the launch key (currency0 = 0x0, currency1 = token, fee 12500, tickSpacing 60, hooks = an address carrying only the beforeInitialize flag, as the canonical guard does), add liquidity, then swap(zeroForOne = true, amountSpecified = -100 ether).

      Expected per brief: 1% of the HIRING output goes to 0xdEaD and hook.totalBurned() > 0.

      Actual: PoolManager never calls HiringFreezeHook (its address is not in the key), hook.totalBurned() == 0 and balanceOf(0xdEaD) == 0.

      Verified with test/scratch/GuardPoolNoBurn.t.sol (fails on this tree: '0 <= 0').

      Conversely initialising a key with hooks = HiringFreezeHook but any other field differing (fee 3000 instead of the constructor fee, for example) reverts with WrongPool via beforeInitialize, so the hook cannot be 'bridged' onto the factory pool either.

    • mediumConstructor reverts at every CREATE2 address whose low 14 bits are not 0x20cc, and the manifest has no field to supply a mined saltsrc/HiringFreezeHook.sol:57

      Hooks.validateHookPermissions reverts with HookAddressNotValid unless uint160(address(this)) & 0x3fff == 0x20cc, i.e. the deployment address must be mined (1 in 16384 salts). The factory deploys application contracts with salts chosen by the service (the protected floor reads IMD_PROJECT_SALT_i from the environment) and the LaunchManifest schema has only contract and constructorArgs per entry, so neither the manifest author nor this source can pin a salt.

      Unless the deployer's salt derivation is extended to search for the 0x20cc suffix against the exact factory address and keccak256(creationCode ++ abi.encode(manager, token, fee)), the hook constructor fails, ProjectDeploymentProbe.deploy reverts with 'project constructor failed', and the whole launch transaction reverts.

      Needed evidence: confirmation from the deployer that it mines hook-compatible salts for this contract, or the hook cannot be listed in contracts at all.

      Call new HiringFreezeHook(manager, token, 12500) with plain CREATE, or CREATE2 with any salt whose predicted address does not end in bits 0x20cc (the existing test test/HiringFreezeHook.t.sol:444-471 searches for exactly such a salt).

      Expected for an ordinary application contract: deployment succeeds.

      Actual: constructor reverts with Hooks.HookAddressNotValid(address) and no code is deployed; under the protected floor, ProjectDeploymentProbe.deploy's require(deployed != address(0) && deployed.code.length > 0, "project constructor failed") fails and the launch is rejected.

      The success path in test/FactoryDeployment.t.sol only works because the test itself brute-forces the salt (lines 30-37), which the real factory is not shown to do.

    • lowThe `fee` constructor argument must equal the pool's runtime LaunchFees value, which the manifest cannot know; any mismatch makes initialisation revert with WrongPoolsrc/HiringFreezeHook.sol:63

      poolId is fixed at construction from the fee argument. The launch guidance says the manifest's pool.fee stays 3000 for admission while the pool actually opens at the network trading fee read from LaunchFees at launch time (12500 by default, and changeable on chain). If the hook is ever made the pool's hook (finding 1), its constructor fee has to equal whatever LaunchFees returns in the launch transaction; the manifest author has to guess that number in advance.

      A wrong guess means beforeInitialize reverts WrongPool and the pool cannot be initialised with this hook; a hook built with 3000 to match the manifest's pool.fee is unusable on a 12500 pool. The constructor accepts any fee up to 1,000,000 so nothing catches the mismatch at deploy time.

      Needed evidence: the manifest/deployer must bind fee to the LaunchFees value the factory will use, or the factory must pass it.

      Deploy HiringFreezeHook(manager, token, 3000) at a valid 0x20cc address.

      Call manager.initialize(PoolKey(0x0, token, 12500, 60, hook), 79228162514264337593543950336).

      Expected (if 12500 is the real launch fee): pool initialised.

      Actual: the modifier computes key.toId() != poolId and reverts WrongPool, bubbled by PoolManager as WrappedError(hook, beforeInitialize.selector, WrongPool, HookCallFailed); test/HiringFreezeHook.t.sol:293-298 shows the same revert for a one-unit fee difference.

      The hook is then permanently unusable for the launch pool and must be redeployed.

    • low`manager` is a raw address argument with no manifest reference form and no code/identity check; a wrong value yields a dead or counter-spoofable hooksrc/HiringFreezeHook.sol:50

      The manifest grammar offers $token, $contract:Name and $owner, none of which can name the chain's PoolManager, so the manifest must hard-code the address string and the constructor accepts anything that is non-zero and not the token (an EOA, a stale or wrong-chain address, or an arbitrary contract).

      With a non-PoolManager address the hook can never be called by the real manager (its pool key embeds the wrong manager implicitly through authentication), so no burn happens; with an attacker-controlled contract at that address its owner can call afterSwap directly and inflate totalBurned to any value without a real trade, which the site displays as 'tokens laid off'.

      This is a manifest/configuration verification gap, not a code bug: the independent review of launch.json must confirm the manager string equals the verified target-chain PoolManager (cast code shows code and it matches the network configuration), and no such configuration was supplied to this stage.

      Deploy HiringFreezeHook(0x1111111111111111111111111111111111111111 (an EOA), token, 12500) at a valid address: constructor succeeds.

      Any real PoolManager pool keyed to this hook reverts OnlyPoolManager in beforeInitialize, so the pool cannot even be initialised and totalBurned stays 0 forever.

      Alternatively deploy with manager = a contract that calls hook.afterSwap(addr, hook.getPoolKey(), SwapParams(true, -1 ether, 1), toBalanceDelta(-1 ether, 1 ether), "") and implements take() by transferring tokens: totalBurned becomes 0.01 ether with no swap, exactly as test/HiringFreezeHook.t.sol:394-403 demonstrates with ReenteringManager.

    • infoHiringToken is a second deployable token artifact with its own ABI export; listing it in the manifest creates a stranded duplicate 10^27 supplysrc/HiringToken.sol:9

      Both LaunchToken and HiringToken compile to deployable contracts with identical name/symbol/supply, and docs/abi/HiringToken.json is exported beside LaunchToken.json.

      The manifest's token must be LaunchToken; if the manifest author also lists HiringToken under contracts (it is a valid, nonpayable, argument-less contract that passes the runtime/opcode floor), the factory ends up holding a second 1,000,000,000 'HIRING' ERC-20 that it cannot move, and two tokens with the same symbol exist on chain. Neither the factory SupplyMismatch check nor the protected floor catches this because they only inspect the launch token.

      The README says to deploy only LaunchToken; the manifest review should enforce it.

      Using test/FactoryDeployment.t.sol's LaunchFactoryProbe, deploy LaunchToken then additionally new HiringToken() from the probe.

      Expected for a correct launch: one token, factory holds 1e27 of it.

      Actual: factory holds 1e27 LaunchToken and 1e27 HiringToken, both named AgentPepeArmyBillion0Humans/HIRING; the HiringToken balance is unreachable (the factory has no transfer logic for it) and a website or indexer keyed on name/symbol may pick the wrong contract.

  4. contracts reviewed
    #1446Audit permissionsClaude5 findings · 1 high
    afterBuild contract project

    The review is complete. The findings file is written and validated, every snippet matches the tree, the scratch tests were removed, and the working tree shows no other changes.

    Outcome. The hook's own permission model and swap accounting hold up: manager authentication, full pool-key binding, the reentrancy flag, the delta-direction and partial-fill checks, and the fee math are consistent across all four swap modes, and the slither and aderyn reentrancy leads are not exploitable because take ends in a plain OpenZeppelin transfer with no callback and the hook holds nothing. The real defects live in the trust gap between this source and the launch path, and all five were reproduced locally against the vendored v4 PoolManager.

    Findings written to .imd-findings.json:

    1. High. The approved 1% burn is unenforceable on the launch pool. The hook only acts on the pool whose key names itself as the hook, while the factory's launch pool is keyed to PoolInitializationGuard, which has no swap callbacks. A swap on that pool burns nothing, the counter stays at zero, and the hook rejects the launch key with WrongPool. Both sides are immutable, so this needs a launch decision, not a source edit. No proof test is attached because none could pass without changing the factory or adding a transfer tax the launch forbids.
    2. Medium. The constructor reverts unless the CREATE2 address carries 0x20cc. The manifest schema has no salt field, so the service must mine the salt against the exact factory, creation code and arguments or the whole launch transaction fails.
    3. Medium. The constructor fee cannot be sourced from the manifest. The pool opens at the LaunchFees trading fee, while the manifest pins 3000. A hook built with the wrong fee makes initialization revert if attached, or binds to a pool that never exists if not.
    4. Low. The manager argument is the sole trust root and unverifiable on chain. A non-canonical manager contract can inflate the burn counter and emit TokensLaidOff events with no HIRING reaching the dead address.
    5. Low. The initializer and price are unrestricted. If the hook ever replaces the guard without atomic deploy, initialize and seed, anyone can open its pool first at an arbitrary price.

    Coverage. All seven listed entry points have rows, plus rows for beforeSwap, beforeInitialize, the constructor and two invariants. The three token entry points on each artifact hold as unmodified OpenZeppelin ERC20 code. No launch.json exists yet, so manifest-level checks on concrete constructor values could only be stated as required evidence rather than verified.

    ran onclaude · claude-fable-5-1 · 32 turns · 8m 11s · 322 in · 31.7K out · 917.1K cached
    submission2b5bf70127cf448897719ae8787cc934a4bbb906cdf5148c81d76c8ed7fc40f0
    devicee382bd4d2b3e471fd1aa383c67bdc7ffed8283726081b64a073c74c6a9449333
    started fromc3a451a3c9400f8b83022a7c3932a595ce73fd15
    bundlenone
    applied on9c3294b83ed1d161797d80b5c52defc293d7ecd1e0c54d57df5a1f08ed11ed8c
    changed · 0 filesnothing
    • highApproved 1% burn is unenforceable on the launch pool: the hook binds itself as the pool's hook, but the factory pool key names PoolInitializationGuardsrc/HiringFreezeHook.sol:69

      Trust gap (access x asymmetry): the only pool HiringFreezeHook will act on is the one whose key has hooks == address(this) (line 69, hashed into the immutable poolId at line 56 and enforced by authenticated at line 63). The canonical launch path (evm-project-launch guidance) opens exactly one pool for the launch token and sets that pool's hook to the factory-supplied PoolInitializationGuard, which has no swap callbacks.

      A Uniswap v4 pool key carries a single hook address, and the hook address is part of the pool id, so the launch pool and the hook's pool are two different pools. Deploying HiringFreezeHook as an application contract therefore attaches nothing to the pool the frontend, the deployer and the admission check describe.

      The approved requirement 'On every swap, 1% of the HIRING side is permanently removed' is not enforced by any deployed code, the totalBurned counter the site must display stays at 0, and because both the hook and the guard pool are immutable there is no post-deployment repair. The builder's README and docs/DEPLOYMENT.md already record this conflict; it remains an unresolved blocking item for the launch, not something the Solidity author can fix in source.

      Needed evidence/decision from services: either an approved factory path that uses HiringFreezeHook (deployed at a mined address, constructed with the real PoolManager and the real pool-key fee) as the launch pool's hook, or an explicit decision to launch without the burn and to remove the burn claim from the brief, README and site.

      No Foundry proof is attached: a test asserting the burn on a guard-keyed pool fails today and can only be made to pass by a change outside this source tree (changing the factory's pool key) or by a transfer-tax token, which the launch rules forbid.

      State: LaunchToken deployed; PoolManager M; HiringFreezeHook H deployed at a 0x20cc-suffixed address with (M, token, fee).

      Launch pool key K_launch = (currency0 = 0x0 ETH, currency1 = token, fee, tickSpacing 60, hooks = PoolInitializationGuard).

      Pool id of K_launch != H.poolId() because hooks differ.

      Steps: (1) M.initialize(K_launch, price); add liquidity.

      (2) Any trader swaps 100e18 HIRING on K_launch via M.unlock -> M.swap(K_launch, SwapParams(zeroForOne=false, amountSpecified=-100e18, limit)).

      Expected per brief: 1e18 HIRING sent to 0xdEaD and H.totalBurned() == 1e18.

      Actual: the guard has no beforeSwap/afterSwap permission bits, H is never called, balanceOf(0xdEaD) unchanged, H.totalBurned() == 0.

      (3) Even a manager call H.afterSwap(sender, K_launch, params, delta, "") reverts WrongPool.

      Reproduced locally with a pool keyed hooks=address(0) as the no-swap-callback stand-in: swap executes, DEAD balance delta 0, totalBurned 0, afterSwap with that key reverts WrongPool, and PoolId(K).toId() != H.poolId().

    • mediumHook constructor reverts unless the CREATE2 address carries 0x20cc; the manifest has no salt field, so the launch fails unless the service mines onesrc/HiringFreezeHook.sol:57

      Trust gap between the source and the deployment inputs. Uniswap v4 only invokes callbacks whose permission bits are encoded in the hook address, so the constructor correctly calls Hooks.validateHookPermissions and reverts with HookAddressNotValid for any address whose low 14 bits are not 0x20cc.

      The LaunchManifest schema for contracts entries has only contract and constructorArgs; there is no salt field, and the protected Project floor takes IMD_PROJECT_SALT_i from the service.

      Unless the deployment service derives a salt by searching against the exact factory address and keccak256(creationCode ++ abi.encode(manager, token, fee)), the probability that an arbitrary salt yields a valid address is 1/16384, and the factory's single launch transaction reverts at the hook constructor, taking the token deployment and pool creation with it. Changing any constructor argument, compiler setting or deploying factory invalidates a mined salt.

      Needed evidence: the service's salt-derivation for this launch (and the protected floor's IMD_PROJECT_SALT_0) shown to produce an address with & 0x3fff == 0x20cc for the final creation code and arguments. This is a service/configuration gap; removing the validation in source is not a fix because the pool manager would then silently skip the callbacks.

      Inputs: manager = any PoolManager address, token = LaunchToken address, fee = 12_500; deployer D; salt s such that address(uint160(uint256(keccak256(0xff ++ D ++ s ++ keccak256(creationCode ++ abi.encode(manager, token, 12_500)))))) & 0x3fff != 0x20cc (e.g. the first such s starting from 0, which is almost always 0).

      Call new HiringFreezeHook{salt: s}(manager, token, 12_500) from D.

      Expected by a plain-salt deployer: contract created.

      Actual: revert Hooks.HookAddressNotValid(predicted).

      Reproduced locally: constructor reverts with exactly that error and the predicted address; the existing test/FactoryDeployment.t.sol only passes because it loops salts until the bits match.

    • mediumConstructor `fee` must equal the launch pool's real LP fee (LaunchFees-derived, 12,500 by default), which the manifest cannot express: pool.fee is pinned at 3000 for admissionsrc/HiringFreezeHook.sol:55

      Asymmetry between the manifest's view of the pool and the hook's immutable view. The hook hashes fee into poolId (lines 55-56) and rejects any other key with WrongPool (line 63). The launch guidance says the manifest's pool.fee stays 3000 for admission while the pool itself opens at the network trading fee read from the chain's LaunchFees contract (1.25% = 12,500 millionths by default, and changeable on chain).

      The hook's fee is a constructor argument chosen at manifest time, so it is fixed before the factory reads LaunchFees.

      If the manifest copies the schema value 3000, or LaunchFees changes between manifest and deployment, then (a) if the hook is ever attached as the pool's hook, the factory's PoolManager.initialize -> beforeInitialize reverts WrongPool and the entire launch transaction fails; (b) if it is not attached (finding 1), the hook is bound to a pool key that is never created. The project tests assume 12_500 but nothing binds that assumption to the chain.

      Needed evidence: the final manifest constructorArgs fee value shown equal to the fee the factory will actually place in the launch pool key at deployment, from the target chain's LaunchFees.

      Inputs: HiringFreezeHook deployed (mined address) with fee = 3000, i.e. the manifest's admission fee.

      The factory opens the pool with key K = (0x0, token, 12_500, 60, hook).

      Call PoolManager.initialize(K, 79228162514264337593543950336).

      Expected by the manifest author: pool initialised with the hook attached.

      Actual: PoolManager calls hook.beforeInitialize(sender, K, price); authenticated compares K.toId() against poolId computed with fee 3000; they differ; revert WrongPool wrapped in Hooks.HookCallFailed; initialize reverts.

      Reproduced locally with a 3000-fee hook and a 12_500-fee key.

    • low`manager` is the hook's sole trust root and is unverifiable on chain: a non-canonical manager contract can inflate totalBurned and emit TokensLaidOff with no HIRING reaching DEADsrc/HiringFreezeHook.sol:61

      Access control x economics seam. Every callback is authenticated only as msg.sender == poolManager, where poolManager is whatever address the manifest's constructorArgs supply (line 53). The constructor rejects only zero and manager == token; it cannot tell the canonical Uniswap v4 PoolManager from any other contract.

      If that argument is anything else (a wrong network's manager, a typo'd address with code, or a contract controlled by whoever writes the manifest), that contract can call afterSwap with fabricated SwapParams and BalanceDelta, and its own take may do nothing. The hook then increments totalBurned and emits TokensLaidOff while no tokens move, so the 'tokens laid off' counter the site displays and any indexer of the event report burns that never happened.

      Real swaps on the real pool would never reach this hook at all. This is not fixable in source beyond a weak code-presence check; it is the concrete reason the final manifest review must bind manager to the target chain's PoolManager from network.json (verified with cast code and a known selector such as protocolFeeController()/unlock), not to $owner or a free-form address.

      Inputs: FakeManager F whose take(Currency,address,uint256) is a no-op.

      Deploy H = HiringFreezeHook(F, token, 12_500) at a mined address.

      F calls H.afterSwap(F, H.getPoolKey(), SwapParams(zeroForOne=true, amountSpecified=-1000e18, sqrtPriceLimitX96=1), toBalanceDelta(-1000e18, 1000e18), "").

      Expected if the counter were trustworthy: 10e18 HIRING at 0xdEaD.

      Actual: H.totalBurned() == 10e18, event TokensLaidOff(poolId, true, 10e18, 10e18) emitted, balanceOf(0xdEaD) unchanged (delta 0), call returns (afterSwap.selector, 10e18).

      Reproduced locally.

    • lowbeforeInitialize restricts only the key, not the initializer or price: if the hook ever replaces the guard, anyone who sees the hook before the pool is opened can initialize it at an arbitrary price asrc/HiringFreezeHook.sol:86

      Access gap relative to the component the hook would have to replace. The factory's PoolInitializationGuard exists to restrict who may initialize the launch pool and at what price. HiringFreezeHook requests the beforeInitialize permission (bit 0x2000) but its implementation (lines 80-87) only checks msg.sender == poolManager and key == poolId; it does not check the initializer (the sender argument is ignored) or the sqrtPriceX96.

      Pool initialization in v4 is permissionless and one-shot.

      If the launch integration is changed so that HiringFreezeHook is the pool's hook (the only way to satisfy finding 1), and the hook is deployed in a transaction separate from pool initialization and seeding, a front-runner can initialize the hook's pool at MIN_SQRT_PRICE+1 (or any price), after which the factory's initialize reverts PoolAlreadyInitialized and the launch fails, or liquidity is seeded at the attacker's price if the deployer does not initialize itself.

      In the current canonical flow (hook deployment, initialization and seeding in one factory transaction) this is not reachable, so it is reported as low; it becomes a requirement on any integration that attaches this hook: deploy, initialize and seed atomically, or have the hook restrict sender to the factory.

      State: hook H deployed at a mined address with (PoolManager M, token, 12_500); H.getPoolKey() pool not yet initialized.

      Attacker A (any EOA) calls M.initialize(H.getPoolKey(), TickMath.MIN_SQRT_PRICE + 1).

      Expected if initialization were guarded like the factory's guard pool: revert.

      Actual: beforeInitialize returns its selector, pool slot0.sqrtPriceX96 == MIN_SQRT_PRICE + 1.

      Then the legitimate M.initialize(H.getPoolKey(), 79228162514264337593543950336) reverts (PoolAlreadyInitialized).

      Reproduced locally.

  5. contracts reviewed
    #351Audit economicsClaude5 findings · 2 high
    afterBuild contract project

    The review is complete. .imd-findings.json holds 5 findings and 14 coverage rows, all snippets verified against the tree, and the scratch tests were removed so no other files were added.

    Outcome. The hook's swap economics are sound. I traced all four swap modes against the vendored v4-core delta accounting, with the protocol fee on, through partial fills, and through reentrancy. The trader always pays the burn, LPs never do, the hook nets to a zero delta, and the burn counter equals what reaches the dead address. The defects are in how the hook meets the launch platform, not in its arithmetic.

    Findings written:

    • High, pool attachment. The hook only authenticates a pool keyed on itself at src/HiringFreezeHook.sol:69. The factory's launch pool is keyed on PoolInitializationGuard, so swaps there never burn and the site's counter stays at zero. Reproduced with a guard stub on the real PoolManager. The constructor fee binding adds a second mismatch between the admission fee of 3000 and the LaunchFees rate.
    • High, CREATE2 address bits. The constructor at line 57 reverts unless the address ends in the permission bits. A factory with a plain salt reverted on 50 of 50 salts in my probe, and the manifest schema has no salt field, so the whole launch transaction would fail. Both high findings need a platform scope decision rather than a source edit, so I attached no proof tests and said why.
    • Low, stuck sells. After a full buyout the manager holds no HIRING, and take to DEAD runs before the seller settles, so every sell reverts until an LP adds HIRING or a router pre-settles. Reproduced with concrete balances.
    • Low, duplicate token. HiringToken is a separately deployable 1e27 token. A manifest that lists it mints a second supply stuck at the factory.
    • Info, dust rounding. Exact-output buys under 99 wei burn nothing. Documented and not profitable.

    Coverage. All seven listed entry points have rows, plus rows for the view callbacks, the constructor, and four invariants. The static-analysis reentrancy leads at lines 101 and 138 were checked and rejected: the only external call is take into a plain OZ ERC-20 with no callbacks, and the burning guard covers every callback.

    What the judge still needs is the manifest itself, which does not exist in this tree, and a verified target-chain PoolManager address, since no network configuration was supplied to this task.

    ran onclaude · claude-fable-5-1 · 33 turns · 8m 14s · 386 in · 34.1K out · 1.3M cached
    submission02f0e8db9ccea1854fd8798dcbba713ce4d6a3c06805bc107a004168546525f8
    deviceca080fd3063996699e20c7e6f185c60d7926b45bca9b079ae2d367ddad7a3eb9
    started fromc3a451a3c9400f8b83022a7c3932a595ce73fd15
    bundlenone
    applied on9c3294b83ed1d161797d80b5c52defc293d7ecd1e0c54d57df5a1f08ed11ed8c
    changed · 0 filesnothing
    • highThe 1% burn cannot execute on the launch pool: the hook authenticates only a pool keyed on itself, while the factory's launch pool is keyed on PoolInitializationGuardsrc/HiringFreezeHook.sol:69

      Flow gap (execution x periphery x first principles). The brief's only application behaviour is 'on every swap on this launch's pool, 1% of the HIRING side goes to DEAD'. A Uniswap v4 pool is identified by its full key, and the key has exactly one hooks address.

      The canonical launch guidance says the factory initialises and seeds the launch pool with its own initialization-only PoolInitializationGuard as the hook and makes no further calls.

      HiringFreezeHook binds its poolId to a key whose hooks field is address(this) (line 69) and reverts WrongPool for any other key, so deploying it as an application contract produces an orphan contract: the factory pool (ETH, HIRING, fee, 60, PoolInitializationGuard) has no swap callbacks and never burns, while the hook's own pool (ETH, HIRING, poolFee, 60, HiringFreezeHook) is a second, unseeded pool that anyone may initialise and seed.

      The website's 'tokens laid off' counter (hook.totalBurned()) therefore stays at 0 for all trading on the launch pool, and the site would misrepresent the launch. A second binding gap compounds this: the constructor's fee argument fixes the key's fee, the manifest fee for admission is 3000, and the pool actually opens at the LaunchFees rate (12,500 by default); a hook built with 3000 keys a third pool.

      The builder's README and docs/DEPLOYMENT.md already flag this conflict; it is recorded here as a blocking review finding because no manifest, policy or factory path in the supplied material attaches a swap hook to the launch pool. No in-repo Solidity change can fix it: either the platform provides an approved path to make HiringFreezeHook the launch pool's hook (and mine its address, see finding 2), or the launch ships without the burn and the README/site must say so.

      That is a scope decision for the requester, so no proof test is attached (no fix in src/ would make one pass).

      State: launch pool initialised by the factory with key (currency0=0x0, currency1=HIRING, fee=12500, tickSpacing=60, hooks=PoolInitializationGuard) and seeded with 800M HIRING; HiringFreezeHook deployed with (manager, HIRING, 12500).

      Input: any swap on the launch pool, e.g. sell 100e18 HIRING exact-input, then buy with 1e18 ETH exact-input.

      Expected per brief: 1e18 HIRING to DEAD after the sell and 1% of the bought HIRING after the buy, totalBurned > 0.

      Actual: DEAD balance 0, hook.totalBurned() == 0; the hook is never called.

      Calling hook.beforeSwap(manager-prank, launchKey, ...) reverts WrongPool.

      Verified locally in a scratch Foundry test using the vendored PoolManager, a guard stub at an address with only the beforeInitialize flag (0x2000), and the real hook: after one sell and one buy on the guard pool, totalBurned == 0 and balanceOf(DEAD) == 0; PoolId of the guard pool != hook.poolId(); and the hook reverts WrongPool for the guard key.

      Also verified that keys with fee 3000 and 12500 yield different PoolIds.

    • highHook constructor reverts at any address whose low 14 bits are not 0x20cc, so a factory that does not mine the CREATE2 salt cannot deploy the launch at allsrc/HiringFreezeHook.sol:57

      Deployment-time economic/flow gap. Uniswap v4 encodes hook permissions in the hook's address; validateHookPermissions reverts HookAddressNotValid unless address & 0x3fff == 0x20cc (beforeInitialize, beforeSwap, afterSwap, beforeSwapReturnDelta, afterSwapReturnDelta). Only 1 in 16,384 CREATE2 salts satisfies this.

      The launch manifest schema carries no salt field (contracts items are {contract, constructorArgs} only), and the canonical guidance says the factory deploys every application contract in one transaction from its own salt derivation.

      Unless that derivation specifically mines for these bits against keccak256(creationCode ++ abi.encode(manager, token, fee)) and the factory address, the hook's constructor reverts, the ProjectDeploymentProbe/factory requires deployed != 0, and the whole launch transaction (token, pool, distributor and all contracts) fails.

      The project floor test (Project.protected.t.sol setUp, 'project constructor failed') would also fail for the service-supplied IMD_PROJECT_SALT_0. docs/DEPLOYMENT.md item 2 names this; it is a concrete constructor conflict for the manifest/service stage.

      There is no Solidity fix that keeps the hook (v4 requires the address bits); the needed evidence is a factory/service salt-mining path for this exact init-code hash and deployer, or the hook must be dropped from the contracts list. No proof test is attached for that reason.

      Input: deploy HiringFreezeHook(manager, token, 3000) via CREATE2 from a factory contract using a plain deterministic salt such as keccak256(abi.encode('launch', i)).

      Expected (for the launch to deploy): constructor succeeds.

      Actual: constructor reverts with Hooks.HookAddressNotValid(address).

      Verified locally: a PlainSaltFactory stand-in deploying with 50 different non-mined salts reverted on all 50 (for each salt the predicted CREATE2 address had low bits != 0x20cc, as it will for 16,383 of every 16,384 salts).

      The project's own tests only succeed because they loop over salts until the bits match (test/HiringFreezeHook.t.sol _deployHook and test/FactoryDeployment.t.sol), which the manifest cannot express.

    • lowEvery sell reverts when the PoolManager holds fewer HIRING than the burn, because take(DEAD) runs before the seller settlessrc/HiringFreezeHook.sol:138

      Flow gap (execution x periphery). In afterSwap the hook calls poolManager.take(HIRING, DEAD, fee), which does an immediate ERC-20 transfer out of the PoolManager's balance. On sells the seller's HIRING is normally settled by the router after swap() returns, so the transfer is paid from whatever HIRING the manager already holds (this pool's reserves plus any other HIRING pools).

      If the pool has been fully bought out (price driven to the top of the seeded range, every HIRING reserve swapped out and 1% burned on each buy), the manager holds effectively no HIRING, and the transfer reverts (ERC20InsufficientBalance), which reverts the swap. This affects exact-input and exact-output sells alike, i.e. exactly the trades that would restore HIRING to the pool.

      Buys yield nothing in that state anyway, so the pool is stuck until an LP adds HIRING liquidity (modifyLiquidity is not hooked) or a router pre-settles the input (sync, transfer, settle before swap), which the Universal Router's default action order does not do. Impact is liveness only, under a specific state, and the README already mentions pre-settling; reported as low so the judge can decide whether the README/site must spell out the recovery path.

      A code-side mitigation that keeps intended behaviour: in afterSwap, instead of take(), credit the fee to the hook and settle the burn to DEAD only when the manager's HIRING balance covers it, otherwise revert with a clearer error; or document that the official router must settle HIRING input before the swap.

      State (scratch test with vendored PoolManager, hook pool at sqrtPrice 2^96): seed a HIRING-only position at ticks [-1200,-600] with liquidity 1000e18 (manager then holds 28.68e18 HIRING); buy with exact-input 1,000,000e18 ETH and price limit MIN_SQRT_PRICE+1 (manager HIRING drops to 1 wei, totalBurned 0.2868e18).

      Input: sell exact-input 100e18 HIRING with price limit MAX_SQRT_PRICE-1 through a router that settles after swap().

      Expected: swap executes, 1e18 HIRING to DEAD, seller receives ETH.

      Actual: afterSwap's take(HIRING, DEAD, 1e18) reverts (manager balance 1 wei) and the whole unlock reverts; no sell can execute until HIRING is added by an LP or pre-settled by the router.

    • lowHiringToken is a second deployable 1e27-supply token with the same name and symbol; listing it in the manifest contracts array mints a duplicate supply to the factorysrc/HiringToken.sol:9

      Manifest/constructor conflict to be guarded against. The brief names the token HiringToken; the launch requires the artifact LaunchToken, so the tree ships both HiringToken (full ERC-20 with a minting constructor) and LaunchToken (inherits it unchanged), and docs/abi/HiringToken.json is exported alongside LaunchToken's ABI as if it were deployable.

      The manifest schema allows any contract name in contracts[]; nothing in source prevents a manifest that lists HiringToken as an application contract. That deployment would create a second 'AgentPepeArmyBillion0Humans (HIRING)' token with 1e27 supply minted to the immutable factory, where it is permanently stuck, and would give explorers and the website two identically-named HIRING tokens.

      The factory's SupplyMismatch check and the protected floor only inspect the LaunchToken balance, so this would not be caught by them. docs/DEPLOYMENT.md says to deploy only LaunchToken; the manifest reviewer must verify contracts[] is exactly [{HiringFreezeHook}] (or empty if finding 1 is resolved by dropping the hook) and token.contract == 'LaunchToken'.

      Input: launch.json with "contracts": [{"contract": "HiringToken", "constructorArgs": []}, {"contract": "HiringFreezeHook", "constructorArgs": ["", "$token", "12500"]}].

      Expected: a single HIRING token with 1e27 supply.

      Actual: HiringToken's constructor (line 9) mints 1e27 of a second HIRING-named token to the factory (msg.sender); the factory holds it forever and the hook may even be pointed at the wrong token if $token were mis-resolved.

      Locally: new HiringToken() from a probe contract yields totalSupply 1e27 and balanceOf(probe) 1e27, independent of LaunchToken (see test/HiringToken.t.sol test_metadataSupplyAndFactoryRecipient, which deploys both).

    • infoBurn rounds down, so swaps of fewer than 100 gross minor units (exact-output buys below 99) burn nothingsrc/HiringFreezeHook.sol:124

      Math precision note, documented by the author in the README and not economically exploitable: fee = floor(gross/100) (or floor(net/99) on exact-output buys) truncates to zero below 100 wei of HIRING, and burns between 0.5% and 1% for gross amounts in [100, 199] wei. At 18 decimals the value avoided is far below gas cost, so splitting trades cannot profitably evade the burn. Recorded for completeness of the rounding review; no change requested.

      State: hook pool with liquidity.

      Input: five exact-output buys of 98 wei HIRING each (490 wei total).

      Expected under a strict '1% of every trade' reading: ~4 wei burned.

      Actual: totalBurned == 0 (verified in a scratch test).

      Dust-level, no compounding.

  6. contracts tested
    #165Write foundry testsCodex5 files changed
    afterBuild contract project
    writes to
    testtest/**

    Added failure-path, boundary, event, and multi-actor invariant tests, entirely under test/.

    Validation passed: forge build and forge test — 47 passed, 0 failed, including 49,152 randomized handler calls.

    No new contract defects reproduced. The existing launch-hook integration conflict and outstanding live-chain checks are documented in test/TESTING.md.

    ran oncodex · gpt-6-astra · 7 turns · 8m 26s · 117.9K in · 18.8K out · 1.9M cached
    submission0bef347736da56aea0aad539299aa42f1b0bff2f24eff50585b57bda699a5f0c
    device8d210756eb9ac4d7a88179dbd4f598adddce273a0df43bd8fc7548cc1f6e2eae
    started fromc3a451a3c9400f8b83022a7c3932a595ce73fd15
    bundlef3fcac079eb9b8caabddadb1ba6b0ac628f6e99a134a76557cfc47ad80e9aa5c · 216 KB
    verifiedrebuilt and matched · verifier 0.1.0 ·
    applied on9c3294b83ed1d161797d80b5c52defc293d7ecd1e0c54d57df5a1f08ed11ed8c
    changed · 5 files
    test/HiringHookBoundaries.t.soltest/HiringPoolInvariant.t.soltest/HiringTokenInvariant.t.soltest/TESTING.mdtest/helpers/HiringPoolFixture.sol
  7. contracts reviewed
    #704Audit mathClaude4 findings · 2 medium
    afterBuild contract project

    Review complete. The findings file is valid JSON with 4 findings and 14 coverage rows, and no tracked file changed. Only ignored paths were touched: the findings file, the scratch tests, and build output.

    What I audited. The Math Precision, Boundary and Numerical Gap passes over the hook's arithmetic and its external boundaries, plus the two token contracts. The vendored v4 core was traced to confirm that afterSwap receives the pre-hook core delta and that the hook's take debit nets against its later credit.

    The math holds. Burn equals floor(gross/100) in all four swap modes. This was confirmed by the project's fuzz suite and by my own 2,000-run fuzz with four overlapping ranges that force tick crossings. The net-specified formula floor(N/99) is provably identical to floor(gross/100) over the whole uint128 range. The int128 bounds, the sign checks, the partial-fill exactness comparison and the zero-net hook delta all behave as intended.

    Findings written to .imd-findings.json:

    • Medium, pool attachment. The hook bakes its own address into the pool id, but the factory's pool uses PoolInitializationGuard as its hook. On that pool the burn never runs and the counter stays at zero. The author already documents this as unresolved. It is the one issue that decides whether the brief's feature exists.
    • Medium, constructor fee binding. The constructor fee must equal the live pool-key fee. The canonical guidance gives both 3000 and the LaunchFees value of 12,500, and only one can be in the key. A mismatch makes initialization revert with WrongPool, reproduced against the real PoolManager.
    • Low, take boundary. The burn transfer needs the manager to already hold the fee before the seller settles. A sale above 100× the manager's HIRING balance reverts through settle-after routers and succeeds when input is settled first. Reproduced, but it needs near-total buyout of the seeded reserve.
    • Info, rounding direction. Burns floor, so trades under 100 minor units burn nothing. Dust, non-compounding, documented.

    Coverage. All seven listed entry points have rows. The token functions hold as unmodified OpenZeppelin v5.1 ERC20. The hook's afterSwap, beforeInitialize and constructor reference findings; beforeSwap and four invariants hold. Nothing in my area was left unreached. No critical or high findings, so no proof files were attached. Static-analysis leads on reentrancy were checked and not promoted: the only external call is an ERC20 transfer to the dead address with no callback, and the flag reset after it is harmless.

    ran onclaude · claude-fable-5-1 · 35 turns · 10m 35s · 290 in · 44.8K out · 968.5K cached
    submission6118bb181a855336148b886ae8280881f3f039e0ba3f7a506b8091925df72e31
    device3b260b68e9ad6a3750b0685b623c35ec00b819afbafb6596714f96b33d486592
    started fromc3a451a3c9400f8b83022a7c3932a595ce73fd15
    bundlenone
    applied on9c3294b83ed1d161797d80b5c52defc293d7ecd1e0c54d57df5a1f08ed11ed8c
    changed · 0 filesnothing
    • mediumHook binds hooks=address(this) into its pool id, so the factory's PoolInitializationGuard pool never calls it and no launch trade burnssrc/HiringFreezeHook.sol:69

      Area: boundary (pool-key identity). The approved workflow requires that every swap on this launch's pool burns 1% of the HIRING side. The hook authenticates the complete pool key, including hooks = address(this), and derives its immutable poolId from it (line 56).

      The canonical launch guidance says the factory supplies PoolInitializationGuard as the pool's hook and that the project must not list it. A v4 PoolKey has exactly one hooks address, and changing it changes the pool id.

      Therefore the pool the factory initializes and seeds (currency0 = ETH, currency1 = $token, fee = LaunchFees value, tickSpacing = 60, hooks = PoolInitializationGuard) has a different id from hook.poolId(), the PoolManager never invokes HiringFreezeHook for that pool, totalBurned stays 0 and TokensLaidOff is never emitted. Deploying HiringFreezeHook as an application contract does not attach it; it also cannot be attached later because the pool id is immutable.

      The builder's README (line 5) and docs/DEPLOYMENT.md (item 1) already flag this as unresolved. It is the single concrete conflict between the accepted source and the launch path, and it decides whether the brief's core feature exists at all. Required evidence before release: an approved launch path whose actual pool key names the deployed HiringFreezeHook address as hooks, or an explicit decision that the launch ships without the burn and the site displays it as unavailable.

      State: factory pool key K1 = (address(0), $token, fee F, 60, hooks = PoolInitializationGuard).

      HiringFreezeHook deployed with (manager, $token, F) computes poolId = keccak256(abi.encode((address(0), $token, F, 60, hooks = hookAddress))) != K1.toId().

      Call: any trader swaps on K1 (e.g. zeroForOne exact-input 1 ether ETH).

      Expected per brief: 1% of the HIRING output transferred to 0x...dEaD, hook.totalBurned() > 0, TokensLaidOff emitted.

      Actual: PoolManager.swap calls K1.hooks (the guard) which has no swap callbacks; HiringFreezeHook is never called; token.balanceOf(0x...dEaD) unchanged; hook.totalBurned() == 0.

      Conversely, if anyone tried to run the swap with the hook's own key, that pool does not exist in the factory flow (pool.checkPoolInitialized reverts PoolNotInitialized) unless an approved path initializes it.

    • mediumImmutable constructor fee must equal the live pool-key fee; canonical manifest fee 3000 and LaunchFees trading fee 12,500 cannot both be right, and a mismatch makes initialization revert WrongPoolsrc/HiringFreezeHook.sol:63

      Area: boundary (constructor input vs. external pool state). The hook's third constructor argument is the static fee that must appear in the pool key, and it is baked into poolId. The launch guidance gives two different fee numbers: the manifest's pool.fee stays 3000 for admission, while the pool itself opens at the network's trading fee read from LaunchFees (12,500 by default, and changeable on chain before the deployment executes).

      Only one value can be in the actual PoolKey. If the manifest writer passes 3000 (matching pool.fee) and the factory initializes with 12,500, or vice versa, beforeInitialize reverts WrongPool inside Hooks.callHook, PoolManager.initialize reverts with WrappedError(hook, beforeInitialize.selector, WrongPool, HookCallFailed), and the launch transaction fails. If the pool is initialized with a different hook (finding 1) the hook is simply orphaned.

      Nothing in the hook or the protected floor validates the constructor fee against the chain, so this is a manifest/constructor input the independent review must pin to the exact fee the factory will use at execution time, read from LaunchFees on the target chain, not from pool.fee. The project tests use 12,500 and docs/DEPLOYMENT.md item 3 names the ambiguity but does not resolve it.

      Reproduced in test/scratch/MathProbe.t.sol test_feeMismatchBricksInitialization (passes, i.e. the revert occurs).

      Steps: deploy LaunchToken and a real vendored PoolManager; mine a 0x20cc-suffixed CREATE2 salt and deploy HiringFreezeHook(manager, token, 3000); build liveKey = PoolKey(address(0), token, 12_500, 60, IHooks(hook)); call manager.initialize(liveKey, 79228162514264337593543950336).

      Expected: pool initialized with the burn hook attached.

      Actual: revert CustomRevert.WrappedError(hook, 0x259982e5 beforeInitialize, abi.encodeWithSelector(HiringFreezeHook.WrongPool.selector), Hooks.HookCallFailed).

      The symmetric case (hook built with 12_500, key with 3000) reverts identically.

      Evidence needed: the exact uint24 fee the factory will place in the pool key for this launch, confirmed against LaunchFees at deployment, bound to the hook's constructorArgs.

    • lowBurn take() needs the PoolManager to already hold the burn amount; a sell whose 1% exceeds the manager's HIRING balance reverts when the router settles after the swapsrc/HiringFreezeHook.sol:138

      Area: boundary (external call return/behaviour). afterSwap moves the burn with a real ERC-20 transfer out of the PoolManager during the swap callback, before the trader has settled the HIRING they are selling. Flash accounting guarantees the hook's net delta is zero, but it does not guarantee the manager's token balance covers fee at that instant. Standard v4 routers (Universal Router V4_SWAP: SWAP_EXACT_IN_SINGLE, then SETTLE_ALL, then TAKE_ALL) settle after the swap.

      When fee = floor(G/100) exceeds token.balanceOf(manager), the ERC-20 transfer inside take reverts, so the whole sell reverts.

      Precondition: a single sale larger than 100x the manager's entire HIRING balance. With the factory's 8e26 seed this requires the pool to have been bought down to under ~1% of supply and a seller holding at least 100x what remains, so it is an extreme state, not a normal path.

      Impact when reached: sells through settle-after routers fail with a transfer error rather than a hook error until someone settles input first or adds HIRING liquidity. No funds are lost; the README line 29 notes the pre-settlement workaround. Minimal fix options that preserve the design: none inside the hook without changing the burn mechanism; document the required router ordering in the handoff, or have the launch frontend route sells with settle-before-swap.

      Reproduced in test/scratch/TakeBoundary.t.sol test_sellLargerThan100xReserveRevertsWhenSettledAfter.

      State: hook pool at sqrtPriceX96 = 2^96 with one full-range position of liquidity 1_000 ether, so manager HIRING balance = 999999999999999999946 (~1000 HIRING).

      Call: unlock -> swap(zeroForOne=false, amountSpecified=-(reserve*100+100), sqrtPriceLimit=MAX-1) then settle.

      With reserve = 999999999999999999946, sale = 99999999999999999994700 and the hook computes fee = floor(sale/100) = 999999999999999999947, one minor unit more than the manager holds.

      Expected per README: sell executes, 1% to DEAD.

      Actual: PoolManager.take -> HIRING transfer from manager fails (balance < fee), swap reverts.

      Same params with the gross input transferred and settled before manager.swap: succeeds and totalBurned == sale/100.

    • infoBurn rounds down, so gross trades under 100 minor units burn nothing and small trades burn strictly less than 1%src/HiringFreezeHook.sol:153

      Area: math precision (rounding direction). Every burn formula floors: floor(G/100) for gross-specified amounts and floor(N/99) for net-specified amounts (line 157, line 124). Fees conventionally round up in favour of the protocol; here rounding favours the trader by at most 1 minor unit per swap and does not compound.

      The two formulas are mutually consistent (verified: floor(N/99) == floor((N + floor(N/99))/100) for all N < 2^128, and burn == floor(gross/100) in all four swap modes across 2,000 fuzz runs with tick crossings). The deviation is dust (1e-18 HIRING per trade) and the README line 22 documents it. Recorded for completeness of the precision pass; no change recommended.

      test/scratch/MathProbe.t.sol test_dustRounding: exact-input sell of 99 minor units -> totalBurned unchanged (0 burned, 0%); exact-input sell of 199 minor units -> burned 1 (0.50% instead of 1%).

      Exact-output buy of 98 units -> gross 98, burned 0; buy of 99 -> gross 100, burned 1.

      Splitting a 100-unit sale into 2x50 saves 1 wei of HIRING at a gas cost far above its value.

  8. contracts reviewed
    #13Audit judgeClaude6 findings · 1 highrevised
    afterBuild contract project, Write foundry tests, Manifest, Audit math, Audit permissions, Audit economics, Audit flow

    Review complete. .imd-findings.json holds 6 findings and 13 coverage rows; no source, config or doc file was changed (only .imd-findings.json and my scratch test test/scratch/Review.t.sol, which reproduces every kept finding and passes alongside the project's 47 tests).

    What I kept, after reproducing each against the code

    1. [high] launch.json:9 — the launch ships no 1% burn. All four specialists reported the same root cause (hook binds hooks = address(this) into its pool id; the factory's pool is keyed on PoolInitializationGuard; a v4 key has one hook slot). I merged them into one finding and anchored it where the consequence actually lives in this stage: the manifest's "contracts": []. The launch as described is a plain token, the approved "on every swap, 1% to dEaD" requirement is unmet, and there is no post-deployment repair. Reproduced with the vendored PoolManager: swaps on a guard-keyed pool leave balanceOf(dEaD) == 0, totalBurned() == 0, and afterSwap with the launch key reverts WrongPool. No Foundry proof attached — no change in src/ can make it pass; the resolution is a service path or a requester decision, both spelled out in the finding.

    2–6. [low] — each a distinct mechanism, each reproduced in test/scratch/Review.t.sol:

    • take() to DEAD runs before the seller settles → sells revert when the manager holds less HIRING than the burn (exact revert: ERC20InsufficientBalance(manager, …946, …947)); same trade succeeds with settle-before-swap. Merged economics + math.
    • Constructor reverts at any unmined CREATE2 address (19/19 plain salts → HookAddressNotValid); manifest has no salt field. Merged 3 specialists.
    • Constructor fee must equal the live key fee; hook built with 3000 + key with 12 500 → initialize reverts. Merged 3 specialists.
    • manager is an unverifiable free-form address; a fake manager inflates totalBurned to 10e18 with nothing moved. Merged 2 specialists.
    • beforeInitialize ignores initializer/price; attacker initialized the hook's pool at MIN_SQRT_PRICE+1, legit initialize then reverts.

    Dropped

    • HiringToken as a second deployable token (2 specialists): the manifest lists no contracts and names LaunchToken — nothing to reproduce as a defect.
    • Rounding (2 specialists, info): floor-to-zero below 100 wei is documented dust, not exploitable.
    • Slither/aderyn reentrancy at afterSwap: totalBurned and the event are written before take(); the token has no callbacks; every callback reverts ReentrantCallback while burning. Not exploitable.

    My own pass beyond the specialists

    • Traced Hooks.beforeSwap/afterSwap delta-sign handling for all four swap modes — the hook's specified/unspecified fee placement and the PartialFill check are correct.
    • Verified the manifest against the schema (all patterns, 1716-char notes, initialPrice < 2^256).
    • Checked vendored lib/ integrity against the pinned upstream commits over the network: OpenZeppelin byte-identical; four v4-core files differ only by forge fmt line-wrapping (whitespace-insensitive diff shows only added braces). Nothing of substance was found that the panel missed.
    ran onclaude · claude-fable-5-1 · 17 turns · 8m 4s · 523 in · 33.9K out · 1.5M cached
    submissione7355daaa62e8060748a54a1ec2c291fba6808fde1b9f60b2f6ec5ba94b95dbd
    device0238a59bba7222372009ab205c0c51a5a37380b7e12f07c8a62b5f2a0dc30ae4
    started from59ce8fbbcad144f2ecb45ceed6d855194ee208e6
    bundlenone
    applied on9c3294b83ed1d161797d80b5c52defc293d7ecd1e0c54d57df5a1f08ed11ed8c, f3fcac079eb9b8caabddadb1ba6b0ac628f6e99a134a76557cfc47ad80e9aa5c, cba5c827e0aa2d2bf51165b9fd8014abb759335f4e864c759354a2d7632d0b9b
    changed · 0 filesnothing
    • highLaunch as described by launch.json ships no 1% burn: the manifest omits HiringFreezeHook because the hook cannot be attached to the factory's guard-keyed pool, so the approved requirement is permanentlaunch.json:9

      Merged root cause of audit_economics #1, audit_flow #1, audit_permissions #1 and audit_math #1. The approved workflow's only application requirement is 'a Uniswap v4 hook on this launch's pool ... On every swap, 1% of the HIRING side is permanently removed'.

      The accepted hook (src/HiringFreezeHook.sol) derives its immutable poolId from a key whose hooks field is address(this) (line 69: PoolKey(Currency.wrap(address(0)), Currency.wrap(hiringToken), poolFee, TICK_SPACING, IHooks(address(this)))) and rejects every other key with WrongPool (line 63).

      The canonical launch opens exactly one pool for the token with the factory-supplied PoolInitializationGuard in the key's single hooks slot; that guard has no swap callbacks, and a v4 pool id is keccak256 of the full key, so the hook can never be invoked by the launch pool and cannot be attached after the fact.

      The manifest contributor therefore wrote "contracts": [], and the launch this manifest describes is a plain token with no burn: totalBurned() has no deployed contract to read from, the required 'tokens laid off' counter can never be live, and the README's headline behaviour does not exist on chain.

      The notes disclose this, but notes carry no deployment authority and the result is still a launch that contradicts the approved brief with no post-deployment repair (pool key and hook are immutable). This is a requirements/policy conflict, not a Solidity bug: no change inside src/ can make a guard-keyed pool call this hook, which is why no Foundry proof is attached.

      Needed before admission: either (a) a service-approved launch path that initialises the launch pool with hooks = the deployed HiringFreezeHook (at a mined 0x20cc address, constructed with the verified PoolManager and the exact fee the factory places in the key; see findings 3-6) and seeds liquidity in the same transaction, after which the manifest must list the hook with constructorArgs [, "$token", ]; or (b) an explicit requester decision to launch without the burn, with the brief, README and site changed to stop claiming it.

      Either way the current manifest cannot be admitted as fulfilling the approved workflow.

      test/scratch/Review.t.sol::test_guardKeyedLaunchPoolNeverBurns (passes = reproduces).

      State: LaunchToken, vendored PoolManager, HiringFreezeHook(manager, token, 12500) mined at a 0x20cc address; launch pool initialised with key (currency0=0x0, currency1=token, fee=12500, tickSpacing=60, hooks=) at sqrtPriceX96 2^96 with 1,000,000e18 liquidity in [-60000,60000].

      Input: buy with exact-input 1e18 ETH, then sell exact-input 100e18 HIRING on that pool.

      Expected per brief: 1e18 HIRING at 0xdEaD after the sell and 1% of the bought HIRING after the buy, hook.totalBurned() > 0.

      Actual: both swaps execute, token.balanceOf(0xdEaD) == 0, hook.totalBurned() == 0; PoolId of the guard-keyed launch key (hooks = 0xA000...2000, beforeInitialize bit only) != hook.poolId(); vm.prank(manager) hook.afterSwap(_, launchKey, ...) reverts WrongPool.

      Manifest state: launch.json line 9 is "contracts": [] and token.contract is LaunchToken, so no hook is deployed at all; the approved burn is absent by construction.

    • lowSells revert when the PoolManager's HIRING balance is below the 1% burn, because afterSwap take()s to DEAD before the seller settlessrc/HiringFreezeHook.sol:138

      Merged from audit_economics #3 and audit_math #3. take() performs an immediate ERC-20 transfer out of the PoolManager. For HIRING-input swaps (exact-input sells and exact-ETH-output sells) the seller's HIRING is normally settled by the router after swap() returns (Universal Router V4_SWAP: swap, then SETTLE_ALL), so at take time the manager only holds pooled reserves.

      When fee = floor(G/100) (or floor(N/99)) exceeds token.balanceOf(manager), the transfer reverts with ERC20InsufficientBalance and the whole swap reverts.

      Precondition: a sell larger than 100x the manager's entire HIRING holdings, i.e. the pool has been bought nearly empty and a large holder sells back; those are precisely the trades that would restore reserves. Liveness only, no funds lost, recoverable by a router that syncs/transfers/settles HIRING before calling swap (shown below) or by any LP adding HIRING.

      The README mentions pre-settlement; the frontend/router handoff must specify it, or the hook must document that only settle-before routers work in that state. Also relevant to any future integration that attaches the hook (finding 1).

      test/scratch/Review.t.sol::test_sellRevertsWhenManagerHoldsLessThanFee.

      State: hook pool at 2^96, one full-range position of liquidity 1000e18, manager HIRING balance R = 999999999999999999946.

      Input: exact-input sell amountSpecified = -(100R + 100), sqrtPriceLimit MAX-1, router settles after swap.

      Expected: swap executes, fee = R+1 = 999999999999999999947 HIRING to DEAD.

      Actual: unlock reverts with WrappedError(hook, afterSwap, WrappedError(token, transfer, ERC20InsufficientBalance(manager, 999999999999999999946, 999999999999999999947), ERC20TransferFailed), HookCallFailed); totalBurned stays 0.

      Same params with sync/transfer(gross)/settle executed before manager.swap: succeeds, totalBurned == balanceOf(DEAD) == (100R+100)/100.

    • lowHook constructor reverts at any CREATE2 address whose low 14 bits are not 0x20cc; the manifest has no salt field, so listing the hook requires the service to mine the factory saltsrc/HiringFreezeHook.sol:57

      Merged from audit_economics #2, audit_flow #2 and audit_permissions #2. Uniswap v4 encodes hook permissions in the address, so this check is correct and must stay; but only 1 in 16,384 salts produces a valid address, and the LaunchManifest schema's contracts items carry only {contract, constructorArgs}.

      The protected floor deploys with IMD_PROJECT_SALT_i from the service and requires deployed != address(0) && deployed.code.length > 0, so an unmined salt fails the floor and would revert the whole factory launch transaction. Not triggered by the current manifest (contracts is empty) but it is a hard precondition for the only resolution path of finding 1 that keeps the burn.

      Needed evidence: the deployer's salt derivation shown to search for addr & 0x3fff == 0x20cc against the exact factory address and keccak256(creationCode ++ abi.encode(manager, token, fee)) for the final compiler output.

      test/scratch/Review.t.sol::test_plainSaltReverts.

      Input: new HiringFreezeHook{salt: s}(manager, token, 12500) from the test contract for s = 0..19, skipping any s whose predicted address already has bits 0x20cc.

      Expected for an ordinary application contract: deployment succeeds.

      Actual: every one of the 19+ attempts reverts with Hooks.HookAddressNotValid(predictedAddress).

      The repo's own tests (test/HiringFreezeHook.t.sol _deployHook, test/FactoryDeployment.t.sol) only pass because they loop over salts until the bits match.

    • lowConstructor `fee` is hashed into the immutable poolId and must equal the exact fee the factory places in the pool key (LaunchFees value, 12,500 by default), not the manifest's admission fee 3000; a misrc/HiringFreezeHook.sol:55

      Merged from audit_flow #3, audit_permissions #3 and audit_math #2. The guidance gives two fee numbers: pool.fee stays 3000 in launch.json for admission while the pool opens at the chain's LaunchFees trading fee (1.25% = 12,500 millionths by default, changeable on chain).

      The hook's fee is fixed at construction and checked by authenticated (line 63) in beforeInitialize, so if the hook is ever placed in the launch pool key with a different fee the factory's initialize reverts and the launch transaction fails; if the manifest copies 3000 from pool.fee the hook is unusable on a 12,500 pool. Nothing in the constructor (it accepts any value <= 1,000,000) or the protected floor validates this.

      Not triggered by the current manifest; it is a binding the manifest/deployer must establish from LaunchFees at deployment time if finding 1 is resolved by attaching the hook.

      test/scratch/Review.t.sol::test_feeMismatchBricksInitialize.

      State: HiringFreezeHook(manager, token, 3000) mined at a valid address.

      Input: manager.initialize(PoolKey(0x0, token, 12500, 60, hook), 79228162514264337593543950336).

      Expected by a manifest author copying pool.fee: pool initialised with the hook attached.

      Actual: revert (WrappedError(hook, beforeInitialize, WrongPool, HookCallFailed)); manager.initialize(hook.getPoolKey(), same price) with fee 3000 succeeds, i.e. the hook is bound to a key the factory would not create.

    • low`manager` is the hook's only trust root and is unverifiable in source; a non-canonical manager address yields a dead hook or lets its owner inflate totalBurned and emit TokensLaidOff without any HIRINsrc/HiringFreezeHook.sol:53

      Merged from audit_flow #4 and audit_permissions #4. Every callback authenticates only msg.sender == address(poolManager) (line 61). The manifest grammar has no reference form for the chain's PoolManager ($token/$contract/$owner only), so the address must be a free-form string, and the constructor rejects only zero and manager == token.

      With a wrong address the real PoolManager can never reach the hook (any pool keyed on it reverts OnlyPoolManager at beforeInitialize); with an attacker-controlled contract at that address its owner can fabricate burns, which the site would display as 'tokens laid off'.

      Not a code defect that source can fix; it is the concrete verification the manifest review must perform if the hook is listed: the manager string must equal the target chain's v4 PoolManager from network configuration, confirmed with cast code and a known selector. No network configuration was supplied to this stage, so no address can be confirmed now.

      test/scratch/Review.t.sol::test_fakeManagerInflatesCounter.

      State: FakeManager F whose take(Currency,address,uint256) is a no-op; H = HiringFreezeHook(F, token, 12500) at a mined address.

      Input: vm.prank(F); H.afterSwap(F, H.getPoolKey(), SwapParams(true, -1000e18, 1), toBalanceDelta(-1000e18, 1000e18), "").

      Expected if the counter were trustworthy: 10e18 HIRING at DEAD.

      Actual: returns (afterSwap.selector, 10e18), H.totalBurned() == 10e18, TokensLaidOff emitted, token.balanceOf(DEAD) == 0.

    • lowbeforeInitialize checks only the key, not the initializer or price: if the hook ever becomes the launch pool's hook and is deployed before initialization, anyone can initialize its pool at an arbitrarsrc/HiringFreezeHook.sol:86

      From audit_permissions #5. The guard the hook would have to replace exists to restrict who initialises the launch pool and at what price; HiringFreezeHook requests the beforeInitialize permission but ignores the sender and sqrtPriceX96 arguments. v4 initialization is permissionless and one-shot. Not reachable in the canonical atomic flow (deploy, initialize, seed in one factory transaction) and not reachable at all under the current manifest.

      It becomes a requirement on any integration that resolves finding 1 by attaching this hook: deployment, initialization at the policy-derived opening price and seeding must be atomic, otherwise a front-runner can brick the launch (PoolAlreadyInitialized) or fix the opening price.

      test/scratch/Review.t.sol::test_permissionlessInitialize.

      State: H mined with (PoolManager, token, 12500), its pool not yet initialised.

      Input: vm.prank(0xBAD); manager.initialize(H.getPoolKey(), TickMath.MIN_SQRT_PRICE + 1).

      Expected if initialization were restricted like the guard pool: revert.

      Actual: succeeds, slot0.sqrtPriceX96 == MIN_SQRT_PRICE + 1; the subsequent manager.initialize(H.getPoolKey(), 2^96) reverts (PoolAlreadyInitialized).

  9. contracts updated
    #52ManifestCodex1 file changedrevised
    afterBuild contract project
    writes to
    launch.json

    Reproduced the missing burn. It cannot be fixed through launch.json under the required factory guard.

    Updated manifest notes and wrote .imd-responses.json, disputing a manifest-only remedy while preserving the finding as unresolved.

    forge build, all 36 existing tests, two reproduction/control tests, and manifest checks passed. Launch remains blocked pending approved hook integration or an explicit requester decision to remove the burn requirement.

    ran oncodex · gpt-6-astra · 4 turns · 4m 9s · 52.9K in · 7.9K out · 531.6K cached
    submissionf87c83b837c90f64594e43100b67d2305eda0d94b5a7da72ad3633e11d725fa6
    device2f1d693f218c1c18c218a1f700bd525389e6df9ee96e2fb77d7677ad58d77dd0
    started from9ab575ec459f9a9ccc89f61c24704c6d071d6724
    bundlecba5c827e0aa2d2bf51165b9fd8014abb759335f4e864c759354a2d7632d0b9b · 206 KB
    verifiedrebuilt and matched · verifier 0.1.0 ·
    applied on9c3294b83ed1d161797d80b5c52defc293d7ecd1e0c54d57df5a1f08ed11ed8c
    changed · 1 file
    launch.json
  10. contracts reviewed
    #978Audit judgeClaude6 findings · 1 high
    afterBuild contract project, Write foundry tests, Manifest, Audit math, Audit permissions, Audit economics, Audit flow

    The review is complete and .imd-findings.json is written with 6 findings and 9 coverage rows.

    Outcome: the high finding is not fixed, and the five advisory findings still hold. Nothing in src/ changed between rounds; the revision added test suites and lengthened the launch.json notes. All 47 shipped tests pass, the ABI export check passes, and no submodule or configuration drift came in.

    Finding 1 (high, kept open). The author's dispute is factually correct: the burn is absent, a manifest-only change cannot attach a swap hook to the factory's guard-keyed pool, and the manifest writer cannot invent a service path or waive the brief. I accept all of that, but it does not close the finding. launch.json still has an empty contracts list, so the launch it describes has no 1% burn, no deployed totalBurned() counter, and no post-deployment repair. I re-ran the reproduction on this tree: a guard-keyed pool executes a buy and a sell with zero sent to DEAD, while the same hook on a hook-keyed pool burns correctly. Closing it requires either a service-approved hook-keyed launch path followed by a manifest revision listing the hook, or an explicit requester decision to launch without the burn with the brief, README and site corrected. Neither is evidenced in this round's inputs.

    Findings 2 to 6 (low, kept). Each re-reproduced in a fresh scratch test against unchanged code:

    • Sells revert when the PoolManager holds less HIRING than the burn because take() runs before the seller settles.
    • The constructor reverts at any address without the 0x20cc permission bits, and the manifest cannot carry a salt.
    • The constructor fee is baked into the pool id and must match the fee the factory writes into the key, not the admission fee 3000.
    • The manager address is the sole trust root; a fake manager inflates totalBurned with nothing reaching DEAD.
    • beforeInitialize ignores the initializer and price, so a non-atomic deployment can be front-run.

    All five are moot if the requester chooses a no-burn launch, and binding only under the hook-keyed resolution. I noted that in each description.

    Coverage. All seven entry points answered. The token functions hold as unmodified OpenZeppelin v5.1.0 ERC-20. The hook's afterSwap is marked as a finding for the take-ordering gap. I rejected the Slither and Aderyn reentrancy leads: the burning flag blocks reentry and take() only reaches the plain token contract. No new defects found.

    ran onclaude · claude-fable-5-1 · 21 turns · 5m 47s · 322 in · 21.1K out · 882.6K cached
    submissionb5e2f05c37bfb4fd4a8f91621e1cf9fc92ecc2b53bab9916973881f9f2907f97
    device09078b7cdfb673fe916742c4a47e1376917d0d103a5cef4a2220f3e2d761fa4c
    started from4e565dd8fad0e8d433ed3d68713d20184569623c
    bundlenone
    applied on9c3294b83ed1d161797d80b5c52defc293d7ecd1e0c54d57df5a1f08ed11ed8c, f3fcac079eb9b8caabddadb1ba6b0ac628f6e99a134a76557cfc47ad80e9aa5c, cba5c827e0aa2d2bf51165b9fd8014abb759335f4e864c759354a2d7632d0b9b
    changed · 0 filesnothing
    • highNOT FIXED: launch.json still describes a launch with no 1% burn; the approved HiringFreezeHook requirement remains unmet and cannot be repaired after deploymentlaunch.json:9

      Second-round settlement of finding f47c3e03 (merged root cause of audit_economics #1, audit_flow #1, audit_permissions #1, audit_math #1). The author's answer is accurate on every factual point and I accept it: the burn is absent, a manifest-only revision cannot attach a swap hook to the factory's guard-keyed pool, and the manifest writer has no authority to invent a service path or waive the brief. None of that closes the finding.

      The tree I am reading is unchanged in src/ (HiringFreezeHook.sol lines 53-69 and 138 are byte-identical to last round) and launch.json still has "contracts": [] with token.contract = LaunchToken; only the notes paragraph grew. The approved workflow's sole application requirement is 'a Uniswap v4 hook on this launch's pool called HiringFreezeHook. On every swap, 1% of the HIRING side is permanently removed'.

      With this manifest the factory opens one pool keyed on PoolInitializationGuard (no swap callbacks), deploys no hook, and the 'tokens laid off' counter the site must show has no contract to read. The pool key is hashed into the pool id and immutable, so there is no post-deployment fix. Notes carry no deployment authority.

      This therefore remains a blocking requirements/policy conflict, not a Solidity defect, and no Foundry proof is attached because no change under src/ can make a guard-keyed pool call the hook (my control test shows the same hook does burn when it is the key's hook).

      What closes it, exactly as stated last round: (a) a service-approved launch path that places the deployed HiringFreezeHook (mined to bits 0x20cc, constructed with the verified target-chain PoolManager and the exact fee the factory writes into the key) in the launch pool key and initialises and seeds that pool atomically, after which launch.json must list the hook with constructorArgs [, "$token", ] and findings 2-6 below become binding; or (b) an explicit requester decision to launch without the burn, with the brief, README and site changed to stop claiming it.

      Neither is evidenced in the inputs to this round. Admission must not treat the current manifest as fulfilling the approved workflow.

      Re-run this round: test/scratch/Review.t.sol::test_guardKeyedLaunchPoolNeverBurns (passes = reproduces) and the positive control test_hookKeyedControlBurnsOnBothSwaps.

      State: LaunchToken; vendored PoolManager; HiringFreezeHook(manager, token, 12500) CREATE2-mined to an address with low bits 0x20cc; launch pool initialised with key (currency0 = 0x0, currency1 = token, fee 12500, tickSpacing 60, hooks = initialization-only stub at 0xa0...2000 carrying only the beforeInitialize bit, as the factory guard does) at sqrtPriceX96 2^96 with 1,000,000e18 liquidity in [-60000, 60000].

      Input: exact-input buy of 1e18 ETH, then exact-input sell of 100e18 HIRING on that pool.

      Expected per brief: 1% of the bought HIRING and 1e18 HIRING at 0xdEaD, hook.totalBurned() > 0.

      Actual: both swaps execute, token.balanceOf(0xdEaD) == 0, hook.totalBurned() == 0, launchKey.toId() != hook.poolId(), and vm.prank(manager) hook.afterSwap(_, launchKey, ...) reverts WrongPool.

      Control on the hook-keyed key with identical liquidity: buy burns > 0, sell burns exactly 1e18, balanceOf(DEAD) == totalBurned.

      Manifest state: launch.json line 9 is "contracts": [], so no hook is deployed at all.

    • lowSTILL HOLDS: sells revert when the PoolManager's HIRING balance is below the 1% burn, because afterSwap take()s to DEAD before the seller settlessrc/HiringFreezeHook.sol:138

      Unchanged code, re-reproduced (merged from audit_economics #3 and audit_math #3). take() performs an immediate ERC-20 transfer out of the PoolManager. For HIRING-input swaps the seller's HIRING is normally settled after swap() returns (Universal Router V4_SWAP order: swap, then SETTLE_ALL), so at take time the manager holds only pooled reserves. When floor(G/100) exceeds token.balanceOf(manager) the transfer reverts and the whole sell reverts.

      Precondition: a sell larger than 100x the manager's entire HIRING holdings, i.e. the pool has been bought nearly empty; those are the trades that would restore reserves. Liveness only, no loss, recoverable by a settle-before router or any LP adding HIRING. Moot under resolution (b) of finding 1; under resolution (a) the frontend/router handoff must specify settle-before-swap for HIRING input, or the README must state that only such routers work in that state.

      test/scratch/Review.t.sol::test_sellRevertsWhenManagerHoldsLessThanFee.

      State: hook pool at 2^96, one full-range position of liquidity 1000e18, manager HIRING balance R = 999999999999999999946.

      Input: exact-input sell amountSpecified = -(100R + 100), sqrtPriceLimit MAX-1, router settles after swap.

      Expected: swap executes, fee = R+1 HIRING to DEAD.

      Actual: unlock reverts (take's ERC20 transfer from the manager fails on balance R < R+1); totalBurned stays 0.

      Same params with sync/transfer(gross)/settle before manager.swap: succeeds, totalBurned == balanceOf(DEAD) == (100R+100)/100.

    • lowSTILL HOLDS: hook constructor reverts at any CREATE2 address whose low 14 bits are not 0x20cc; the manifest has no salt field, so listing the hook requires the service to mine the factory saltsrc/HiringFreezeHook.sol:57

      Unchanged code, re-reproduced (merged from audit_economics #2, audit_flow #2, audit_permissions #2). Uniswap v4 encodes hook permissions in the address, so this check is correct and must stay; but only 1 in 16,384 salts yields a valid address and the LaunchManifest contracts items carry only {contract, constructorArgs}.

      The protected floor deploys with the service's IMD_PROJECT_SALT_i and requires deployed code, so an unmined salt fails the floor and reverts the whole factory launch transaction. Not triggered by the current manifest (contracts is empty); it is a hard precondition of resolution (a) of finding 1.

      Needed evidence: the deployer's salt derivation shown to search for addr & 0x3fff == 0x20cc against the exact factory address and keccak256(creationCode ++ abi.encode(manager, token, fee)) for the final compiler output.

      test/scratch/Review.t.sol::test_plainSaltReverts.

      Input: new HiringFreezeHook{salt: s}(manager, token, 12500) from the test contract for s = 0..19, skipping any s whose predicted address already has bits 0x20cc.

      Expected for an ordinary application contract: deployment succeeds.

      Actual: every one of the 19+ attempts reverts with Hooks.HookAddressNotValid(predictedAddress).

      The repo's own tests (test/helpers/HiringPoolFixture.sol _setUpPool, test/FactoryDeployment.t.sol) only pass because they loop over salts until the bits match.

    • lowSTILL HOLDS: constructor `fee` is hashed into the immutable poolId and must equal the exact fee the factory places in the pool key (LaunchFees value, 12,500 by default), not the manifest's admission fsrc/HiringFreezeHook.sol:55

      Unchanged code, re-reproduced (merged from audit_flow #3, audit_permissions #3, audit_math #2). The guidance gives two fee numbers: pool.fee stays 3000 in launch.json for admission while the pool opens at the chain's LaunchFees trading fee (12,500 millionths by default, changeable on chain).

      The hook's fee is fixed at construction and enforced by authenticated (line 63) in beforeInitialize, so a hook placed in the launch key with a different fee makes the factory's initialize revert and the launch transaction fail; a hook built with 3000 is unusable on a 12,500 pool. Nothing in the constructor (any value <= 1,000,000 accepted) or the protected floor validates this.

      Not triggered by the current manifest; under resolution (a) the manifest/deployer must bind this argument to the LaunchFees value at deployment time.

      test/scratch/Review.t.sol::test_feeMismatchBricksInitialize.

      State: HiringFreezeHook(manager, token, 3000) mined at a valid address.

      Input: manager.initialize(PoolKey(0x0, token, 12500, 60, hook), 79228162514264337593543950336).

      Expected by a manifest author copying pool.fee: pool initialised with the hook attached.

      Actual: revert (WrappedError(hook, beforeInitialize, WrongPool, HookCallFailed)); manager.initialize(hook.getPoolKey(), same price) with fee 3000 succeeds, i.e. the hook is bound to a key the factory would not create.

    • lowSTILL HOLDS: `manager` is the hook's only trust root and is unverifiable in source; a non-canonical manager yields a dead hook or lets its owner inflate totalBurned and emit TokensLaidOff without any src/HiringFreezeHook.sol:53

      Unchanged code, re-reproduced (merged from audit_flow #4 and audit_permissions #4). Every callback authenticates only msg.sender == address(poolManager) (line 61). The manifest grammar has no reference form for the chain's PoolManager, so the address must be a free-form string, and the constructor rejects only zero and manager == token.

      With a wrong address the real PoolManager can never reach the hook; with an attacker-controlled contract at that address its owner can fabricate burns, which the site would display as 'tokens laid off'. Not fixable in source; it is the concrete verification the manifest review must perform under resolution (a): the manager string must equal the target chain's v4 PoolManager from network configuration, confirmed with cast code and a known selector.

      No network configuration was supplied to this stage.

      test/scratch/Review.t.sol::test_fakeManagerInflatesCounter.

      State: FakeManager F whose take(Currency,address,uint256) is a no-op; H = HiringFreezeHook(F, token, 12500) at a mined address.

      Input: vm.prank(F); H.afterSwap(F, H.getPoolKey(), SwapParams(true, -1000e18, 1), toBalanceDelta(-1000e18, 1000e18), "").

      Expected if the counter were trustworthy: 10e18 HIRING at DEAD.

      Actual: returns (afterSwap.selector, 10e18), H.totalBurned() == 10e18, TokensLaidOff emitted, token.balanceOf(DEAD) == 0.

    • lowSTILL HOLDS: beforeInitialize checks only the key, not the initializer or price; if the hook ever becomes the launch pool's hook and is deployed before initialization, anyone can initialize its pool asrc/HiringFreezeHook.sol:86

      Unchanged code, re-reproduced (from audit_permissions #5). The guard the hook would have to replace exists to restrict who initialises the launch pool and at what price; HiringFreezeHook requests the beforeInitialize permission but ignores the sender and sqrtPriceX96 arguments. v4 initialization is permissionless and one-shot. Not reachable in the canonical atomic flow and not reachable at all under the current manifest.

      Under resolution (a) of finding 1 it is a requirement on the integration: deployment, initialization at the policy-derived opening price and seeding must be atomic, otherwise a front-runner can brick the launch (PoolAlreadyInitialized) or fix the opening price.

      test/scratch/Review.t.sol::test_permissionlessInitialize.

      State: H mined with (PoolManager, token, 12500), its pool not yet initialised.

      Input: vm.prank(0xBAD); manager.initialize(H.getPoolKey(), TickMath.MIN_SQRT_PRICE + 1).

      Expected if initialization were restricted like the guard pool: revert.

      Actual: succeeds, slot0.sqrtPriceX96 == MIN_SQRT_PRICE + 1; the subsequent manager.initialize(H.getPoolKey(), 2^96) reverts (PoolAlreadyInitialized).

  11. contracts updated
    #263ManifestCodexruntime erroron the agent's machine: the task produced no changes; the agent's last message was: Blocked by the environment: every command fails …retried on #1773 (Codex)

    the task produced no changes; the agent's last message was: Blocked by the environment: every command fails with bwrap: No permissions to create a new namespace.

    Required reads, forge build, and forge test could not run. Both attempts to write .imd-responses.json also failed.

    No files were changed. The burn finding remains unresolved; the sandbox must be repaired before this revision can proceed.

    ran oncodex · gpt-6-astra · 3 turns · 55s · 9.7K in · 1.7K out · 86.1K cached
    submission34ea6c9d48b523136dc6c6b0646bd848d25e9455b5308df2a00f53b9c5eda375
    device2122058046fede33f4ac641f832bfb989ed32544d02994360b57eb8d698ceca7
    started from1ba3dff785111217516a6f5eec2709622bb9e1d1
    bundlenone
    applied on9c3294b83ed1d161797d80b5c52defc293d7ecd1e0c54d57df5a1f08ed11ed8c
    changed · 0 filesnothing
    #1773Codexruntime erroron the agent's machine: the task produced no changes; the agent's last message was: Blocked by the environment: commands fail before …retried on #83 (Codex)

    the task produced no changes; the agent's last message was: Blocked by the environment: commands fail before execution with bwrap: No permissions to create a new namespace.

    Required reads, forge build, and forge test could not run. Writing .imd-responses.json also failed.

    No files changed. The burn finding remains unresolved; the execution environment needs repair before this revision can proceed.

    ran oncodex · gpt-6-astra · 3 turns · 40s · 20.2K in · 1.3K out · 55.2K cached
    submission1c8b27050971517eaef2d28dd044c162669c2b1210d87b753d3fb80c1a4d5a84
    device5095407bdf7ee98aad508060f3043b78e6c50c9d17620f088f6f0348267fc527
    started from1ba3dff785111217516a6f5eec2709622bb9e1d1
    bundlenone
    applied on9c3294b83ed1d161797d80b5c52defc293d7ecd1e0c54d57df5a1f08ed11ed8c
    changed · 0 filesnothing
    #83Codexruntime erroron the agent's machine: the task produced no changes; the agent's last message was: Blocked by the tool environment: …
    afterBuild contract project
    writes to
    launch.json

    the task produced no changes; the agent's last message was: Blocked by the tool environment: /home/ubuntu/.local/bin/codex-code-mode-host is missing, so commands cannot start.

    No files changed. I couldn’t read the required inputs, reproduce the finding, write .imd-responses.json, or run forge build and forge test. The burn finding remains unresolved; the tool runner needs repair before this revision can proceed.

    ran oncodex · gpt-6-astra · 2 turns · 25s · 7.8K in · 714 out · 47.5K cached
    submission6517a6423279b8461863da46df89062742148e38a72c7f625c1f65520525c47d
    devicea7785f55ff5b9988e3db6d05250b713ca78daed1256490391d9bcd5129de8c5c
    started from1ba3dff785111217516a6f5eec2709622bb9e1d1
    bundlenone
    applied on9c3294b83ed1d161797d80b5c52defc293d7ecd1e0c54d57df5a1f08ed11ed8c
    changed · 0 filesnothing
  12. contracts publishedidentity-md-launches/launch-755-workflow-contract-stage-context
  13. deployedWorkflow paused. Waiting for independent review and GitHub publication.
  14. website builtafter deployment
  15. website publishedafter the website is accepted
  16. hostedWorkflow paused. Waiting for the website build and GitHub publication.
  17. checkednot reached