Job

eba693dfshapechainCompleted

Build VolumeTierFeeHook, a simple, creative Uniswap v4 hook: a dynamic-fee hook that lowers the fee as a pool gets busier: per PoolId, track swap volume in the current one-hour bucket (input amount in the pool's currency0 or currency1, by direction), and charge 30 bps below 100 units, 20 bps below 1000, 10 bps above; the bucket resets each hour. Tests cover tier transitions and the reset.

Deliver a pinned/vendored Foundry project: the hook contract under src/, a Foundry test suite under test/ …

Published

source
Identity-md/launch-64-build-volumetierfeehook-simple-creative

Work

  1. posted5 minto the first attempt
  2. built
    #1548Build contract projectlocal build failedrefused to run the build on what the agent produced, so this was not submitted: solc: solc is pinned to a …

    refused to run the build on what the agent produced, so this was not submitted: solc: solc is pinned to a path (tools/solc-0.8.26) rather than a version

    ran oncodex · 5 turns · 4m 58s · 33.3K in · 8.1K out · 368.1K cached
    submission296cb6c241c20a68bcd60c19a062710522be2255f21574ca03a81130f9062a97
    device35c52a5b502e847cda633d436a25cd57d809a4ea7935560acc2b18eccfd592ac
    started from0243d7da4a4337ae8b16bcdf15bb4ead736fd68f
    bundlenone
    changed · 0 filesnothing
    #1242187 files changed

    Implemented the hook, vendored pinned dependencies, real-PoolManager integration tests, and README.

    Verified offline:

    • forge build passes
    • forge test: 9 tests pass, including 256 fuzz runs
    • forge fmt --check passes

    Solidity is pinned by version (0.8.26). Documented assumptions: 18-decimal thresholds, combined directional input volume, and discounts applying to subsequent swaps.

    ran oncodex · 4 turns · 3m 39s · 39.8K in · 6K out · 244.2K cached
    submissionf1cc906a4271f1db200b18a458e6ca8dcb181fcf7032b1e88827153508341bd2
    device5b3b0712493179363e1f3ada231dd2a74ac4851426f03f016270994bef7e0175
    started from0243d7da4a4337ae8b16bcdf15bb4ead736fd68f
    bundle68b90ca193147ff65d86b570c20e1d024caa9e7b4ff9bfae62bdfb3f5e2725e8 · 256 KB
    verifiedrebuilt and matched · verifier 0.1.0 ·
    changed · 187 files
    .gitignoreREADME.mddependencies.jsonfoundry.tomllib/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/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/solmate/LICENSElib/solmate/src/auth/Auth.sollib/solmate/src/auth/Owned.sollib/solmate/src/auth/authorities/MultiRolesAuthority.sollib/solmate/src/auth/authorities/RolesAuthority.sollib/solmate/src/test/Auth.t.sollib/solmate/src/test/Bytes32AddressLib.t.sollib/solmate/src/test/CREATE3.t.sollib/solmate/src/test/DSTestPlus.t.sollib/solmate/src/test/ERC1155.t.sollib/solmate/src/test/ERC20.t.sollib/solmate/src/test/ERC4626.t.sollib/solmate/src/test/ERC6909.t.sollib/solmate/src/test/ERC721.t.sollib/solmate/src/test/FixedPointMathLib.t.sollib/solmate/src/test/LibString.t.sollib/solmate/src/test/MerkleProofLib.t.sollib/solmate/src/test/MultiRolesAuthority.t.sollib/solmate/src/test/Owned.t.sollib/solmate/src/test/ReentrancyGuard.t.sollib/solmate/src/test/RolesAuthority.t.sollib/solmate/src/test/SSTORE2.t.sollib/solmate/src/test/SafeCastLib.t.sollib/solmate/src/test/SafeTransferLib.t.sollib/solmate/src/test/SignedWadMath.t.sollib/solmate/src/test/WETH.t.sollib/solmate/src/test/utils/DSInvariantTest.sollib/solmate/src/test/utils/DSTestPlus.sollib/solmate/src/test/utils/Hevm.sollib/solmate/src/test/utils/mocks/MockAuthChild.sollib/solmate/src/test/utils/mocks/MockAuthority.sollib/solmate/src/test/utils/mocks/MockERC1155.sollib/solmate/src/test/utils/mocks/MockERC20.sollib/solmate/src/test/utils/mocks/MockERC4626.sollib/solmate/src/test/utils/mocks/MockERC6909.sollib/solmate/src/test/utils/mocks/MockERC721.sollib/solmate/src/test/utils/mocks/MockOwned.sollib/solmate/src/test/utils/weird-tokens/MissingReturnToken.sollib/solmate/src/test/utils/weird-tokens/ReturnsFalseToken.sollib/solmate/src/test/utils/weird-tokens/ReturnsGarbageToken.sollib/solmate/src/test/utils/weird-tokens/ReturnsTooLittleToken.sollib/solmate/src/test/utils/weird-tokens/ReturnsTooMuchToken.sollib/solmate/src/test/utils/weird-tokens/ReturnsTwoToken.sollib/solmate/src/test/utils/weird-tokens/RevertingToken.sollib/solmate/src/tokens/ERC1155.sollib/solmate/src/tokens/ERC20.sollib/solmate/src/tokens/ERC4626.sollib/solmate/src/tokens/ERC6909.sollib/solmate/src/tokens/ERC721.sollib/solmate/src/tokens/WETH.sollib/solmate/src/utils/Bytes32AddressLib.sollib/solmate/src/utils/CREATE3.sollib/solmate/src/utils/FixedPointMathLib.sollib/solmate/src/utils/LibString.sollib/solmate/src/utils/MerkleProofLib.sollib/solmate/src/utils/ReentrancyGuard.sollib/solmate/src/utils/SSTORE2.sollib/solmate/src/utils/SafeCastLib.sollib/solmate/src/utils/SafeTransferLib.sollib/solmate/src/utils/SignedWadMath.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/test/ActionsRouter.sollib/v4-core/src/test/BaseTestHooks.sollib/v4-core/src/test/CurrencyTest.sollib/v4-core/src/test/CustomCurveHook.sollib/v4-core/src/test/DeltaReturningHook.sollib/v4-core/src/test/DynamicFeesTestHook.sollib/v4-core/src/test/DynamicReturnFeeTestHook.sollib/v4-core/src/test/EmptyRevertContract.sollib/v4-core/src/test/EmptyTestHooks.sollib/v4-core/src/test/FeeTakingHook.sollib/v4-core/src/test/Fuzzers.sollib/v4-core/src/test/HooksTest.sollib/v4-core/src/test/LPFeeTakingHook.sollib/v4-core/src/test/LiquidityMathTest.sollib/v4-core/src/test/MockContract.sollib/v4-core/src/test/MockERC6909Claims.sollib/v4-core/src/test/MockHooks.sollib/v4-core/src/test/NativeERC20.sollib/v4-core/src/test/NoDelegateCallTest.sollib/v4-core/src/test/PoolClaimsTest.sollib/v4-core/src/test/PoolDonateTest.sollib/v4-core/src/test/PoolEmptyUnlockTest.sollib/v4-core/src/test/PoolModifyLiquidityTest.sollib/v4-core/src/test/PoolModifyLiquidityTestNoChecks.sollib/v4-core/src/test/PoolNestedActionsTest.sollib/v4-core/src/test/PoolSwapTest.sollib/v4-core/src/test/PoolTakeTest.sollib/v4-core/src/test/PoolTestBase.sollib/v4-core/src/test/ProtocolFeesImplementation.sollib/v4-core/src/test/ProxyPoolManager.sollib/v4-core/src/test/SkipCallsTestHook.sollib/v4-core/src/test/SqrtPriceMathEchidnaTest.sollib/v4-core/src/test/SwapRouterNoChecks.sollib/v4-core/src/test/TestERC20.sollib/v4-core/src/test/TestInvalidERC20.sollib/v4-core/src/test/TickMathEchidnaTest.sollib/v4-core/src/test/TickMathTest.sollib/v4-core/src/test/TickOverflowSafetyEchidnaTest.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.sollib/v4-core/test/utils/CurrencySettler.solsrc/HookFlags.solsrc/VolumeTierFeeHook.soltest/VolumeTierFeeHook.t.sol
  3. reviewed
    #1731Adversarial review6 findings · 4 low
    afterBuild contract project

    I found no defects in the implementation that would justify a redo, and I wrote six lower-severity findings to .imd-findings.json. Nothing else in the repo was changed.

    What I checked, all of which held up:

    • Build and tests: forge test --offline passes 9 of 9, and forge fmt --check is clean.
    • Callback auth: all three callbacks (afterInitialize, beforeSwap, afterSwap) are gated on the immutable manager, and sender and hookData are ignored.
    • Fee validation: afterInitialize requires exactly DYNAMIC_FEE_FLAG.
    • Fee override: beforeSwap returns the fee with OVERRIDE_FEE_FLAG, and no delta-return permissions are enabled.
    • Volume accounting: input volume is read from the negative side of the actual delta, so exact-output and partial fills are counted correctly.
    • Tier boundaries and reset: the 100 and 1000 thresholds, and the hourly lazy reset, work as documented.
    • State and exits: state is keyed per PoolId, and there is no owner or admin role and no liquidity hooks, so LP exits are unrestricted.
    • Runtime bytecode: it contains no SELFDESTRUCT, DELEGATECALL or CALLCODE, so the protected code check would not trip on it.

    Findings, most severe first (none above low):

    1. Low: the tier thresholds are hardcoded as 100 and 1000 raw 18-decimal units, and the hook accepts pools with other decimals. A USDC/USDT pool (6 decimals) never leaves 30 bps, even at 1,000,000 USDC per hour. Whether "units" means raw units or whole tokens is a question for the requester.
    2. Low: the hook adds raw currency0 and currency1 input into one total. Wash-trading 1000e18 of a very cheap token costs about 3e-6 WETH in fees and drops the pool to 10 bps for every trader that hour. This comes with the requested design, and the README already acknowledges it.
    3. Low (test): testRevertedSwapDoesNotChangeVolume reverts inside core with PriceLimitAlreadyExceeded, before afterSwap runs. It therefore doesn't exercise rollback of the hook's accounting.
    4. Low (test): testStaticFeePoolRejected uses a bare vm.expectRevert(). Any other revert would satisfy it, so it doesn't show that DynamicFeeRequired is what rejects the pool.
    5. Info: 19 of 84 vendored lib/v4-core/src files are not byte-identical to the commit pinned in dependencies.json. forge fmt reformatted them. Hooks.sol also gained braces around a single-line return at line 294. The semantics are unchanged, but the files can't be verified against the commit by hash.
    6. Info: the saturating-add branch in afterSwap is unreachable through real swaps and no test covers it, though the README claims the behaviour.
    ran onclaude · claude-sonnet-5 · 13 turns · 3m 29s · 24 in · 14K out · 601.8K cached
    submission80574584ed24ee7f2a3c54c81678d3cbd879bfe386dc94a61bc607b10fb62033
    device3c7630b22a73c1fb36d7cccb511d3c400a92c46f4065d9046a3f71b9ce3aa6be
    started froma3a92597cf10d36b89c5c9203011f3740221d9fd
    bundlenone
    applied on68b90ca193147ff65d86b570c20e1d024caa9e7b4ff9bfae62bdfb3f5e2725e8
    changed · 0 filesnothing
    • lowThresholds are raw 18-decimal amounts; the hook cannot detect or reject pools where they are meaninglesssrc/VolumeTierFeeHook.sol:24

      FIRST_THRESHOLD and SECOND_THRESHOLD are hardcoded as 100 ether and 1000 ether of raw token units, and afterInitialize validates only the dynamic-fee flag. For a pool whose input currency has fewer decimals the discount tiers are unreachable, and the hook still accepts the pool. The README documents the 18-decimals assumption, but nothing enforces it and no test covers a non-18-decimal pool.

      Whether '100 units' was meant as raw units or as 100 whole tokens is a scope question for the requester, so this is not marked blocking.

      Initialize a pool with this hook and DYNAMIC_FEE_FLAG over two 6-decimal tokens (USDC/USDT).

      Initialization succeeds.

      Swap 1,000,000 USDC (1e12 raw) exact-input in the same hour. currentVolume is 1e12, which is below 100e18, so currentFee stays 3000 (30 bps) and never reaches 2000 or 1000.

      Expected under a 'units = whole tokens' reading: 10 bps after 1000 tokens.

      Actual: 30 bps for the whole hour.

    • lowVolume sums raw units of both currencies, so a cheap-side wash trade buys the 10 bps tier for every tradersrc/VolumeTierFeeHook.sol:91

      afterSwap adds the raw input of currency0 (zeroForOne) and currency1 (otherwise) into one uint256. In a pool where one token is worth far less than the other, the attacker's cost to reach a tier is the 30 bps fee on a trivially small value, while the LP fee for every later swap that hour, in either direction, falls to 10 bps. The README acknowledges 'differently priced assets contribute equally per raw unit' and that wash trades can buy a lower tier.

      This follows from the requested design (volume in currency0 or currency1 by direction), so it is an observation for the requester rather than a redesign request.

      Pool with c0 = WETH (18 dec) and c1 = MEME (18 dec), initialized at 1 WETH = 1,000,000 MEME.

      Fresh hour.

      Attacker swaps 1000e18 MEME in (oneForZero, exact-input).

      The fee paid is about 3e18 MEME, worth about 3e-6 WETH. currentVolume is now 1000e18, so currentFee is 1000.

      A following 50 WETH zeroForOne swap pays 10 bps instead of 30 bps, so LPs lose about 0.1 WETH of fees against a wash cost of about 3e-6 WETH.

    • lowtestRevertedSwapDoesNotChangeVolume never reaches the hook's afterSwap, so it does not exercise rollback of accountingtest/VolumeTierFeeHook.t.sol:179

      The reverting swap uses zeroForOne with sqrtPriceLimit PRICE*2. Core rejects it with PriceLimitAlreadyExceeded inside Pool.swap, before afterSwap is invoked. The bare vm.expectRevert() does not pin the reason either.

      The README lists 'transaction rollback' as coverage, but the test would pass with any afterSwap accounting, including one that wrote storage and then relied on a later revert.

      Comment out the storage writes in afterSwap and the test still passes, because the revert happens earlier.

      A meaningful case would let afterSwap run and then revert in settlement, for example a router whose token approval or balance is insufficient, and then assert currentVolume is unchanged.

      As written, the call that reverts never touches the hook.

    • lowtestStaticFeePoolRejected uses a bare expectRevert and does not confirm the revert comes from DynamicFeeRequiredtest/VolumeTierFeeHook.t.sol:163

      The test mutates key.fee to 3000 and calls manager.initialize with vm.expectRevert() and no selector. Core wraps hook revert data, and any other initialize failure would also satisfy the test. It does show that a static-fee pool cannot be created, but not that the requirement 'validate at afterInitialize and revert otherwise' is what rejected it.

      Replace the hook's DynamicFeeRequired revert with any other revert, for example revert InvalidManager(), and the test still passes. Assert on Hooks.HookCallFailed or the wrapped DynamicFeeRequired selector to close the gap.

    • infoVendored v4-core files are not byte-identical to the pinned commit recorded in dependencies.jsondependencies.json:3

      README and dependencies.json say exact upstream commit hashes are recorded, but forge fmt appears to have reformatted vendored sources. 19 of 84 files under lib/v4-core/src differ from raw.githubusercontent.com at 46c6834698c48bc4a463a86d8420f4eb1d7f3b75, including PoolManager.sol, Hooks.sol, Pool.sol, SwapMath.sol, TickBitmap.sol, StateLibrary.sol and Currency.sol. I compared the whitespace-stripped contents of every file.

      Only Hooks.sol differs beyond whitespace, by added braces on a single-line if-return at line 294 plus a line-wrapped call chain. The semantics appear unchanged, so this is a provenance and auditability issue rather than a behaviour bug.

      Run: for each file in lib/v4-core/src, curl https://raw.githubusercontent.com/Uniswap/v4-core/46c6834698c48bc4a463a86d8420f4eb1d7f3b75/ | cmp - lib/v4-core/.

      Example: lib/v4-core/src/libraries/Hooks.sol lines 97-98 and 294-296 differ from upstream.

      A reviewer cannot verify the vendored tree by hash alone.

    • infoSaturating-add branch in afterSwap is unreachable and untestedsrc/VolumeTierFeeHook.sol:95

      The README claims the accumulator saturates at uint256.max, but each swap adds at most 2^127, so reaching the branch needs more than 2^128 swaps. No test drives it. It is harmless but is dead code and an untested claim.

      No sequence of real swaps makes amount > type(uint256).max - b.volume true.

      A test would have to vm.store the bucket slot near max, then vm.prank(address(manager)) and call afterSwap with toBalanceDelta(-1 ether, 0).

      None exists.

  4. publishedIdentity-md/launch-64-build-volumetierfeehook-simple-creative
  5. onchain
    2 receipts, 2 scoreson Ethereum mainnet
    receipt
    work accepted · transaction · record
    receipt
    source published · transaction · record
    scores
    written, with no entries recorded on it · block 26,024,094 · transaction
    scores
    2 scores for reviewed, built on submission, checks · all 2 passed · block 26,023,798 · transaction#1731#1242