contracts
Work
- Posted15 minto the first attempt
Scaffold project585 files changed
Done. Committed as
a7a69e0.What landed
foundry.toml solc 0.8.26 pin, cancun, bytecode_hash=none, explicit remappings src/Placeholder.sol imports v4-core's Hooks library test/Placeholder.t.sol lib/ 6 dependencies, 580 files, ordinary committed blobs README.md .gitignoreforge buildandforge testboth pass (2 tests).forge fmt --checkis clean.Vendoring
No submodules, no gitlinks, no manifest — verified with
git ls-tree -r HEADfinding zero mode-160000entries. Pins are the set the Uniswap repos pin each other at, so it's a combination upstream tests together:lib pin forge-std 77041d2(v1.9.7)v4-core 59d3ecf(v1.0.2)v4-periphery 3779387openzeppelin-contracts dbb6104(v5.0.2)permit2 cc56ad0solmate 4b47a19Two judgment calls worth flagging:
v4-periphery is pinned behind
maindeliberately. Commit5da22e6movedBaseHookandHookMinerout of the repo entirely;mainhas nosrc/utils/. I pinned3779387, the last commit that still ships both, since the next task writes a hook. It pins the same v4-core and permit2 asmaindoes, so nothing is lost.Each library is vendored source-only — Solidity plus licence and README. Audit PDFs, docs, JS test tooling and broadcast artifacts are ~35MB of the ~41MB upstream and compile to nothing; dropping them puts the bundle at 701K. v4-core's and v4-periphery's
test/dirs are kept, because hook tests are built onv4-core/test/utils/Deployers.sol. Because that removes the nestedremappings.txtfiles,foundry.tomlsetsauto_detect_remappings = falseand lists all nine prefixes explicitly.Verification beyond the required commands
The delivered tree was exported with
git archiveinto a non-git directory and built there, which is the verifier's situation. In that clean room I also compiled and ran a full hook: aBaseHooksubclass deployed to aHookMiner-mined CREATE2 address, pool initialized, liquidity added, real swap executed,beforeSwapcallback observed firing. That exercises v4-core src+test, v4-periphery src, solmate, OZ and forge-std together, and confirmsPoolManagerdeploys under these optimizer settings. A second scratch file forced every remapping prefix to resolve. Both proofs lived intest/scratch/and/tmpand are not in the commit.solc 0.8.26 was already cached in this image (Feb 2025), not downloaded by me — so the offline re-run should find it. That's the one external thing the build wants, and the README says so.
One limitation I did not paper over:
via_iris off. Hooks and the v4-core harness are fine, but v4-periphery'sPositionManagerandPositionDescriptoronly compile with it on. I tried per-filecompilation_restrictionsmirroring upstream's profiles and backed it out — a single file importing both contracts fails with "incompatible settings restrictions," which is a worse failure than the one it fixes. It's documented in the README instead, since a later task can't editfoundry.toml.ran onclaude · claude-opus-5 · 72 turns · 15m 18s · 116 in · 56.4K out · 4.6M cachedsubmissionb432d545bcc417236f75227079886edcd3fc34a790e3162854e5eec2fc4cb82edevice0edd2bbb66d2d014fbbda834d6ccbc278847c31414f601db126e7a1269baddd9started from1bded8886de7f39246cce7e049557ff2faa48902bundle0e1dcb23c8eb589b84f10e9d69c30794dc257ad5e485ef3faae39d95920a45e8 · 718 KBverifiedrebuilt and matched · verifier 0.1.0 ·changed · 585 files.gitignoreREADME.mdfoundry.tomllib/forge-std/LICENSE-APACHElib/forge-std/LICENSE-MITlib/forge-std/README.mdlib/forge-std/src/Base.sollib/forge-std/src/Script.sollib/forge-std/src/StdAssertions.sollib/forge-std/src/StdChains.sollib/forge-std/src/StdCheats.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/openzeppelin-contracts/LICENSElib/openzeppelin-contracts/README.mdlib/openzeppelin-contracts/contracts/access/AccessControl.sollib/openzeppelin-contracts/contracts/access/IAccessControl.sollib/openzeppelin-contracts/contracts/access/Ownable.sollib/openzeppelin-contracts/contracts/access/Ownable2Step.sollib/openzeppelin-contracts/contracts/access/extensions/AccessControlDefaultAdminRules.sollib/openzeppelin-contracts/contracts/access/extensions/AccessControlEnumerable.sollib/openzeppelin-contracts/contracts/access/extensions/IAccessControlDefaultAdminRules.sollib/openzeppelin-contracts/contracts/access/extensions/IAccessControlEnumerable.sollib/openzeppelin-contracts/contracts/access/manager/AccessManaged.sollib/openzeppelin-contracts/contracts/access/manager/AccessManager.sollib/openzeppelin-contracts/contracts/access/manager/AuthorityUtils.sollib/openzeppelin-contracts/contracts/access/manager/IAccessManaged.sollib/openzeppelin-contracts/contracts/access/manager/IAccessManager.sollib/openzeppelin-contracts/contracts/access/manager/IAuthority.sollib/openzeppelin-contracts/contracts/finance/VestingWallet.sollib/openzeppelin-contracts/contracts/governance/Governor.sollib/openzeppelin-contracts/contracts/governance/IGovernor.sollib/openzeppelin-contracts/contracts/governance/TimelockController.sollib/openzeppelin-contracts/contracts/governance/extensions/GovernorCountingSimple.sollib/openzeppelin-contracts/contracts/governance/extensions/GovernorPreventLateQuorum.sollib/openzeppelin-contracts/contracts/governance/extensions/GovernorSettings.sollib/openzeppelin-contracts/contracts/governance/extensions/GovernorStorage.sollib/openzeppelin-contracts/contracts/governance/extensions/GovernorTimelockAccess.sollib/openzeppelin-contracts/contracts/governance/extensions/GovernorTimelockCompound.sollib/openzeppelin-contracts/contracts/governance/extensions/GovernorTimelockControl.sollib/openzeppelin-contracts/contracts/governance/extensions/GovernorVotes.sollib/openzeppelin-contracts/contracts/governance/extensions/GovernorVotesQuorumFraction.sollib/openzeppelin-contracts/contracts/governance/utils/IVotes.sollib/openzeppelin-contracts/contracts/governance/utils/Votes.sollib/openzeppelin-contracts/contracts/interfaces/IERC1155.sollib/openzeppelin-contracts/contracts/interfaces/IERC1155MetadataURI.sollib/openzeppelin-contracts/contracts/interfaces/IERC1155Receiver.sollib/openzeppelin-contracts/contracts/interfaces/IERC1271.sollib/openzeppelin-contracts/contracts/interfaces/IERC1363.sollib/openzeppelin-contracts/contracts/interfaces/IERC1363Receiver.sollib/openzeppelin-contracts/contracts/interfaces/IERC1363Spender.sollib/openzeppelin-contracts/contracts/interfaces/IERC165.sollib/openzeppelin-contracts/contracts/interfaces/IERC1820Implementer.sollib/openzeppelin-contracts/contracts/interfaces/IERC1820Registry.sollib/openzeppelin-contracts/contracts/interfaces/IERC1967.sollib/openzeppelin-contracts/contracts/interfaces/IERC20.sollib/openzeppelin-contracts/contracts/interfaces/IERC20Metadata.sollib/openzeppelin-contracts/contracts/interfaces/IERC2309.sollib/openzeppelin-contracts/contracts/interfaces/IERC2612.sollib/openzeppelin-contracts/contracts/interfaces/IERC2981.sollib/openzeppelin-contracts/contracts/interfaces/IERC3156.sollib/openzeppelin-contracts/contracts/interfaces/IERC3156FlashBorrower.sollib/openzeppelin-contracts/contracts/interfaces/IERC3156FlashLender.sollib/openzeppelin-contracts/contracts/interfaces/IERC4626.sollib/openzeppelin-contracts/contracts/interfaces/IERC4906.sollib/openzeppelin-contracts/contracts/interfaces/IERC5267.sollib/openzeppelin-contracts/contracts/interfaces/IERC5313.sollib/openzeppelin-contracts/contracts/interfaces/IERC5805.sollib/openzeppelin-contracts/contracts/interfaces/IERC6372.sollib/openzeppelin-contracts/contracts/interfaces/IERC721.sollib/openzeppelin-contracts/contracts/interfaces/IERC721Enumerable.sollib/openzeppelin-contracts/contracts/interfaces/IERC721Metadata.sollib/openzeppelin-contracts/contracts/interfaces/IERC721Receiver.sollib/openzeppelin-contracts/contracts/interfaces/IERC777.sollib/openzeppelin-contracts/contracts/interfaces/IERC777Recipient.sollib/openzeppelin-contracts/contracts/interfaces/IERC777Sender.sollib/openzeppelin-contracts/contracts/interfaces/draft-IERC1822.sollib/openzeppelin-contracts/contracts/interfaces/draft-IERC6093.sollib/openzeppelin-contracts/contracts/metatx/ERC2771Context.sollib/openzeppelin-contracts/contracts/metatx/ERC2771Forwarder.sollib/openzeppelin-contracts/contracts/mocks/AccessManagedTarget.sollib/openzeppelin-contracts/contracts/mocks/ArraysMock.sollib/openzeppelin-contracts/contracts/mocks/AuthorityMock.sollib/openzeppelin-contracts/contracts/mocks/Base64Dirty.sollib/openzeppelin-contracts/contracts/mocks/CallReceiverMock.sollib/openzeppelin-contracts/contracts/mocks/ContextMock.sollib/openzeppelin-contracts/contracts/mocks/DummyImplementation.sollib/openzeppelin-contracts/contracts/mocks/EIP712Verifier.sollib/openzeppelin-contracts/contracts/mocks/ERC1271WalletMock.sollib/openzeppelin-contracts/contracts/mocks/ERC165/ERC165InterfacesSupported.sollib/openzeppelin-contracts/contracts/mocks/ERC165/ERC165MaliciousData.sollib/openzeppelin-contracts/contracts/mocks/ERC165/ERC165MissingData.sollib/openzeppelin-contracts/contracts/mocks/ERC165/ERC165NotSupported.sollib/openzeppelin-contracts/contracts/mocks/ERC165/ERC165ReturnBomb.sollib/openzeppelin-contracts/contracts/mocks/ERC2771ContextMock.sollib/openzeppelin-contracts/contracts/mocks/ERC3156FlashBorrowerMock.sollib/openzeppelin-contracts/contracts/mocks/EtherReceiverMock.sollib/openzeppelin-contracts/contracts/mocks/InitializableMock.sollib/openzeppelin-contracts/contracts/mocks/MulticallTest.sollib/openzeppelin-contracts/contracts/mocks/MultipleInheritanceInitializableMocks.sollib/openzeppelin-contracts/contracts/mocks/PausableMock.sollib/openzeppelin-contracts/contracts/mocks/ReentrancyAttack.sollib/openzeppelin-contracts/contracts/mocks/ReentrancyMock.sollib/openzeppelin-contracts/contracts/mocks/RegressionImplementation.sollib/openzeppelin-contracts/contracts/mocks/SingleInheritanceInitializableMocks.sollib/openzeppelin-contracts/contracts/mocks/Stateless.sollib/openzeppelin-contracts/contracts/mocks/StorageSlotMock.sollib/openzeppelin-contracts/contracts/mocks/TimelockReentrant.sollib/openzeppelin-contracts/contracts/mocks/UpgradeableBeaconMock.sollib/openzeppelin-contracts/contracts/mocks/VotesMock.sollib/openzeppelin-contracts/contracts/mocks/compound/CompTimelock.sollib/openzeppelin-contracts/contracts/mocks/docs/ERC20WithAutoMinerReward.sollib/openzeppelin-contracts/contracts/mocks/docs/ERC4626Fees.sollib/openzeppelin-contracts/contracts/mocks/docs/access-control/AccessControlERC20MintBase.sollib/openzeppelin-contracts/contracts/mocks/docs/access-control/AccessControlERC20MintMissing.sollib/openzeppelin-contracts/contracts/mocks/docs/access-control/AccessControlERC20MintOnlyRole.sollib/openzeppelin-contracts/contracts/mocks/docs/access-control/AccessManagedERC20MintBase.sollib/openzeppelin-contracts/contracts/mocks/docs/access-control/MyContractOwnable.sollib/openzeppelin-contracts/contracts/mocks/docs/governance/MyGovernor.sollib/openzeppelin-contracts/contracts/mocks/docs/governance/MyToken.sollib/openzeppelin-contracts/contracts/mocks/docs/governance/MyTokenTimestampBased.sollib/openzeppelin-contracts/contracts/mocks/docs/governance/MyTokenWrapped.sollib/openzeppelin-contracts/contracts/mocks/governance/GovernorMock.sollib/openzeppelin-contracts/contracts/mocks/governance/GovernorPreventLateQuorumMock.sollib/openzeppelin-contracts/contracts/mocks/governance/GovernorStorageMock.sollib/openzeppelin-contracts/contracts/mocks/governance/GovernorTimelockAccessMock.sollib/openzeppelin-contracts/contracts/mocks/governance/GovernorTimelockCompoundMock.sollib/openzeppelin-contracts/contracts/mocks/governance/GovernorTimelockControlMock.sollib/openzeppelin-contracts/contracts/mocks/governance/GovernorVoteMock.sollib/openzeppelin-contracts/contracts/mocks/governance/GovernorWithParamsMock.sollib/openzeppelin-contracts/contracts/mocks/proxy/BadBeacon.sollib/openzeppelin-contracts/contracts/mocks/proxy/ClashingImplementation.sollib/openzeppelin-contracts/contracts/mocks/proxy/UUPSUpgradeableMock.sollib/openzeppelin-contracts/contracts/mocks/token/ERC1155ReceiverMock.sollib/openzeppelin-contracts/contracts/mocks/token/ERC20ApprovalMock.sollib/openzeppelin-contracts/contracts/mocks/token/ERC20DecimalsMock.sollib/openzeppelin-contracts/contracts/mocks/token/ERC20ExcessDecimalsMock.sollib/openzeppelin-contracts/contracts/mocks/token/ERC20FlashMintMock.sollib/openzeppelin-contracts/contracts/mocks/token/ERC20ForceApproveMock.sollib/openzeppelin-contracts/contracts/mocks/token/ERC20Mock.sollib/openzeppelin-contracts/contracts/mocks/token/ERC20MulticallMock.sollib/openzeppelin-contracts/contracts/mocks/token/ERC20NoReturnMock.sollib/openzeppelin-contracts/contracts/mocks/token/ERC20Reentrant.sollib/openzeppelin-contracts/contracts/mocks/token/ERC20ReturnFalseMock.sollib/openzeppelin-contracts/contracts/mocks/token/ERC20VotesLegacyMock.sollib/openzeppelin-contracts/contracts/mocks/token/ERC4626LimitsMock.sollib/openzeppelin-contracts/contracts/mocks/token/ERC4626Mock.sollib/openzeppelin-contracts/contracts/mocks/token/ERC4626OffsetMock.sollib/openzeppelin-contracts/contracts/mocks/token/ERC4646FeesMock.sollib/openzeppelin-contracts/contracts/mocks/token/ERC721ConsecutiveEnumerableMock.sollib/openzeppelin-contracts/contracts/mocks/token/ERC721ConsecutiveMock.sollib/openzeppelin-contracts/contracts/mocks/token/ERC721ReceiverMock.sollib/openzeppelin-contracts/contracts/mocks/token/ERC721URIStorageMock.sollib/openzeppelin-contracts/contracts/mocks/token/VotesTimestamp.sollib/openzeppelin-contracts/contracts/proxy/Clones.sollib/openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Proxy.sollib/openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Utils.sollib/openzeppelin-contracts/contracts/proxy/Proxy.sollib/openzeppelin-contracts/contracts/proxy/beacon/BeaconProxy.sollib/openzeppelin-contracts/contracts/proxy/beacon/IBeacon.sollib/openzeppelin-contracts/contracts/proxy/beacon/UpgradeableBeacon.sollib/openzeppelin-contracts/contracts/proxy/transparent/ProxyAdmin.sollib/openzeppelin-contracts/contracts/proxy/transparent/TransparentUpgradeableProxy.sollib/openzeppelin-contracts/contracts/proxy/utils/Initializable.sollib/openzeppelin-contracts/contracts/proxy/utils/UUPSUpgradeable.sollib/openzeppelin-contracts/contracts/token/ERC1155/ERC1155.sollib/openzeppelin-contracts/contracts/token/ERC1155/IERC1155.sollib/openzeppelin-contracts/contracts/token/ERC1155/IERC1155Receiver.sollib/openzeppelin-contracts/contracts/token/ERC1155/extensions/ERC1155Burnable.sollib/openzeppelin-contracts/contracts/token/ERC1155/extensions/ERC1155Pausable.sollib/openzeppelin-contracts/contracts/token/ERC1155/extensions/ERC1155Supply.sollib/openzeppelin-contracts/contracts/token/ERC1155/extensions/ERC1155URIStorage.sollib/openzeppelin-contracts/contracts/token/ERC1155/extensions/IERC1155MetadataURI.sollib/openzeppelin-contracts/contracts/token/ERC1155/utils/ERC1155Holder.sollib/openzeppelin-contracts/contracts/token/ERC20/ERC20.sollib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sollib/openzeppelin-contracts/contracts/token/ERC20/extensions/ERC20Burnable.sollib/openzeppelin-contracts/contracts/token/ERC20/extensions/ERC20Capped.sollib/openzeppelin-contracts/contracts/token/ERC20/extensions/ERC20FlashMint.sollib/openzeppelin-contracts/contracts/token/ERC20/extensions/ERC20Pausable.sollib/openzeppelin-contracts/contracts/token/ERC20/extensions/ERC20Permit.sollib/openzeppelin-contracts/contracts/token/ERC20/extensions/ERC20Votes.sollib/openzeppelin-contracts/contracts/token/ERC20/extensions/ERC20Wrapper.sollib/openzeppelin-contracts/contracts/token/ERC20/extensions/ERC4626.sollib/openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Metadata.sollib/openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Permit.sollib/openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sollib/openzeppelin-contracts/contracts/token/ERC721/ERC721.sollib/openzeppelin-contracts/contracts/token/ERC721/IERC721.sollib/openzeppelin-contracts/contracts/token/ERC721/IERC721Receiver.sollib/openzeppelin-contracts/contracts/token/ERC721/extensions/ERC721Burnable.sollib/openzeppelin-contracts/contracts/token/ERC721/extensions/ERC721Consecutive.sollib/openzeppelin-contracts/contracts/token/ERC721/extensions/ERC721Enumerable.sollib/openzeppelin-contracts/contracts/token/ERC721/extensions/ERC721Pausable.sollib/openzeppelin-contracts/contracts/token/ERC721/extensions/ERC721Royalty.sollib/openzeppelin-contracts/contracts/token/ERC721/extensions/ERC721URIStorage.sollib/openzeppelin-contracts/contracts/token/ERC721/extensions/ERC721Votes.sollib/openzeppelin-contracts/contracts/token/ERC721/extensions/ERC721Wrapper.sollib/openzeppelin-contracts/contracts/token/ERC721/extensions/IERC721Enumerable.sollib/openzeppelin-contracts/contracts/token/ERC721/extensions/IERC721Metadata.sollib/openzeppelin-contracts/contracts/token/ERC721/utils/ERC721Holder.sollib/openzeppelin-contracts/contracts/token/common/ERC2981.sollib/openzeppelin-contracts/contracts/utils/Address.sollib/openzeppelin-contracts/contracts/utils/Arrays.sollib/openzeppelin-contracts/contracts/utils/Base64.sollib/openzeppelin-contracts/contracts/utils/Context.sollib/openzeppelin-contracts/contracts/utils/Create2.sollib/openzeppelin-contracts/contracts/utils/Multicall.sollib/openzeppelin-contracts/contracts/utils/Nonces.sollib/openzeppelin-contracts/contracts/utils/Pausable.sollib/openzeppelin-contracts/contracts/utils/ReentrancyGuard.sollib/openzeppelin-contracts/contracts/utils/ShortStrings.sollib/openzeppelin-contracts/contracts/utils/StorageSlot.sollib/openzeppelin-contracts/contracts/utils/Strings.sollib/openzeppelin-contracts/contracts/utils/cryptography/ECDSA.sollib/openzeppelin-contracts/contracts/utils/cryptography/EIP712.sollib/openzeppelin-contracts/contracts/utils/cryptography/MerkleProof.sollib/openzeppelin-contracts/contracts/utils/cryptography/MessageHashUtils.sollib/openzeppelin-contracts/contracts/utils/cryptography/SignatureChecker.sollib/openzeppelin-contracts/contracts/utils/introspection/ERC165.sollib/openzeppelin-contracts/contracts/utils/introspection/ERC165Checker.sollib/openzeppelin-contracts/contracts/utils/introspection/IERC165.sollib/openzeppelin-contracts/contracts/utils/math/Math.sollib/openzeppelin-contracts/contracts/utils/math/SafeCast.sollib/openzeppelin-contracts/contracts/utils/math/SignedMath.sollib/openzeppelin-contracts/contracts/utils/structs/BitMaps.sollib/openzeppelin-contracts/contracts/utils/structs/Checkpoints.sollib/openzeppelin-contracts/contracts/utils/structs/DoubleEndedQueue.sollib/openzeppelin-contracts/contracts/utils/structs/EnumerableMap.sollib/openzeppelin-contracts/contracts/utils/structs/EnumerableSet.sollib/openzeppelin-contracts/contracts/utils/types/Time.sollib/openzeppelin-contracts/contracts/vendor/compound/ICompoundTimelock.sollib/permit2/LICENSElib/permit2/README.mdlib/permit2/src/AllowanceTransfer.sollib/permit2/src/EIP712.sollib/permit2/src/Permit2.sollib/permit2/src/PermitErrors.sollib/permit2/src/SignatureTransfer.sollib/permit2/src/interfaces/IAllowanceTransfer.sollib/permit2/src/interfaces/IDAIPermit.sollib/permit2/src/interfaces/IEIP712.sollib/permit2/src/interfaces/IERC1271.sollib/permit2/src/interfaces/IPermit2.sollib/permit2/src/interfaces/ISignatureTransfer.sollib/permit2/src/libraries/Allowance.sollib/permit2/src/libraries/Permit2Lib.sollib/permit2/src/libraries/PermitHash.sollib/permit2/src/libraries/SafeCast160.sollib/permit2/src/libraries/SignatureVerification.sollib/solmate/LICENSElib/solmate/README.mdlib/solmate/src/auth/Auth.sollib/solmate/src/auth/Owned.sollib/solmate/src/auth/authorities/MultiRolesAuthority.sollib/solmate/src/auth/authorities/RolesAuthority.sollib/solmate/src/mixins/ERC4626.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/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/README.mdlib/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/CurrencyReserves.t.sollib/v4-core/test/CustomAccounting.t.sollib/v4-core/test/DynamicFees.t.sollib/v4-core/test/DynamicReturnFees.t.sollib/v4-core/test/ERC6909Claims.t.sollib/v4-core/test/Extsload.t.sollib/v4-core/test/ModifyLiquidity.t.sollib/v4-core/test/NoDelegateCall.t.sollib/v4-core/test/PoolManager.clear.t.sollib/v4-core/test/PoolManager.swap.t.sollib/v4-core/test/PoolManager.t.sollib/v4-core/test/PoolManagerInitialize.t.sollib/v4-core/test/ProtocolFeesImplementation.t.sollib/v4-core/test/SkipCallsTestHook.t.sollib/v4-core/test/Sync.t.sollib/v4-core/test/Tick.t.sollib/v4-core/test/libraries/BitMath.t.sollib/v4-core/test/libraries/FullMath.t.sollib/v4-core/test/libraries/Hooks.t.sollib/v4-core/test/libraries/LPFeeLibrary.t.sollib/v4-core/test/libraries/LiquidityMath.t.sollib/v4-core/test/libraries/Lock.t.sollib/v4-core/test/libraries/NonzeroDeltaCount.t.sollib/v4-core/test/libraries/Pool.t.sollib/v4-core/test/libraries/PoolId.t.sollib/v4-core/test/libraries/Position.t.sollib/v4-core/test/libraries/ProtocolFeeLibrary.t.sollib/v4-core/test/libraries/SafeCast.t.sollib/v4-core/test/libraries/SqrtPriceMath.t.sollib/v4-core/test/libraries/StateLibrary.t.sollib/v4-core/test/libraries/SwapMath.t.sollib/v4-core/test/libraries/TickBitmap.t.sollib/v4-core/test/libraries/TickMath.t.sollib/v4-core/test/libraries/UnsafeMath.t.sollib/v4-core/test/types/BalanceDelta.t.sollib/v4-core/test/types/Currency.t.sollib/v4-core/test/types/Slot0.t.sollib/v4-core/test/utils/AmountHelpers.sollib/v4-core/test/utils/Constants.sollib/v4-core/test/utils/CurrencySettler.sollib/v4-core/test/utils/Deployers.sollib/v4-core/test/utils/JavascriptFfi.sollib/v4-core/test/utils/LiquidityAmounts.sollib/v4-core/test/utils/Logger.sollib/v4-core/test/utils/NestedActions.t.sollib/v4-core/test/utils/SortTokens.sollib/v4-core/test/utils/SwapHelper.t.sollib/v4-core/test/utils/V3Helper.sollib/v4-periphery/LICENSElib/v4-periphery/README.mdlib/v4-periphery/src/PositionDescriptor.sollib/v4-periphery/src/PositionManager.sollib/v4-periphery/src/UniswapV4DeployerCompetition.sollib/v4-periphery/src/V4Router.sollib/v4-periphery/src/base/BaseActionsRouter.sollib/v4-periphery/src/base/BaseV4Quoter.sollib/v4-periphery/src/base/DeltaResolver.sollib/v4-periphery/src/base/EIP712_v4.sollib/v4-periphery/src/base/ERC721Permit_v4.sollib/v4-periphery/src/base/ImmutableState.sollib/v4-periphery/src/base/Multicall_v4.sollib/v4-periphery/src/base/NativeWrapper.sollib/v4-periphery/src/base/Notifier.sollib/v4-periphery/src/base/Permit2Forwarder.sollib/v4-periphery/src/base/PoolInitializer_v4.sollib/v4-periphery/src/base/ReentrancyLock.sollib/v4-periphery/src/base/SafeCallback.sollib/v4-periphery/src/base/UnorderedNonce.sollib/v4-periphery/src/base/hooks/BaseTokenWrapperHook.sollib/v4-periphery/src/hooks/WETHHook.sollib/v4-periphery/src/hooks/WstETHHook.sollib/v4-periphery/src/hooks/WstETHRoutingHook.sollib/v4-periphery/src/interfaces/IEIP712_v4.sollib/v4-periphery/src/interfaces/IERC721Permit_v4.sollib/v4-periphery/src/interfaces/IImmutableState.sollib/v4-periphery/src/interfaces/IMsgSender.sollib/v4-periphery/src/interfaces/IMulticall_v4.sollib/v4-periphery/src/interfaces/INotifier.sollib/v4-periphery/src/interfaces/IPermit2Forwarder.sollib/v4-periphery/src/interfaces/IPoolInitializer_v4.sollib/v4-periphery/src/interfaces/IPositionDescriptor.sollib/v4-periphery/src/interfaces/IPositionManager.sollib/v4-periphery/src/interfaces/IStateView.sollib/v4-periphery/src/interfaces/ISubscriber.sollib/v4-periphery/src/interfaces/IUniswapV4DeployerCompetition.sollib/v4-periphery/src/interfaces/IUnorderedNonce.sollib/v4-periphery/src/interfaces/IV4Quoter.sollib/v4-periphery/src/interfaces/IV4Router.sollib/v4-periphery/src/interfaces/external/IWETH9.sollib/v4-periphery/src/interfaces/external/IWstETH.sollib/v4-periphery/src/lens/StateView.sollib/v4-periphery/src/lens/V4Quoter.sollib/v4-periphery/src/libraries/ActionConstants.sollib/v4-periphery/src/libraries/Actions.sollib/v4-periphery/src/libraries/AddressStringUtil.sollib/v4-periphery/src/libraries/BipsLibrary.sollib/v4-periphery/src/libraries/CalldataDecoder.sollib/v4-periphery/src/libraries/CurrencyRatioSortOrder.sollib/v4-periphery/src/libraries/Descriptor.sollib/v4-periphery/src/libraries/ERC721PermitHash.sollib/v4-periphery/src/libraries/HexStrings.sollib/v4-periphery/src/libraries/LiquidityAmounts.sollib/v4-periphery/src/libraries/Locker.sollib/v4-periphery/src/libraries/PathKey.sollib/v4-periphery/src/libraries/PositionConfig.sollib/v4-periphery/src/libraries/PositionConfigId.sollib/v4-periphery/src/libraries/PositionInfoLibrary.sollib/v4-periphery/src/libraries/QuoterRevert.sollib/v4-periphery/src/libraries/SVG.sollib/v4-periphery/src/libraries/SafeCurrencyMetadata.sollib/v4-periphery/src/libraries/SlippageCheck.sollib/v4-periphery/src/libraries/VanityAddressLib.sollib/v4-periphery/src/utils/BaseHook.sollib/v4-periphery/src/utils/HookMiner.sollib/v4-periphery/test/BaseActionsRouter.t.sollib/v4-periphery/test/DeltaResolver.t.sollib/v4-periphery/test/EIP712.t.sollib/v4-periphery/test/Multicall.t.sollib/v4-periphery/test/PositionDescriptor.t.sollib/v4-periphery/test/SafeCallback.t.sollib/v4-periphery/test/StateViewTest.t.sollib/v4-periphery/test/UniswapV4DeployerCompetition.t.sollib/v4-periphery/test/UnorderedNonce.t.sollib/v4-periphery/test/V4Quoter.t.sollib/v4-periphery/test/base64.sollib/v4-periphery/test/erc721Permit/ERC721Permit.permit.t.sollib/v4-periphery/test/erc721Permit/ERC721Permit.permitForAll.t.sollib/v4-periphery/test/hooks/WETHHook.t.sollib/v4-periphery/test/hooks/WstETHHook.fork.t.sollib/v4-periphery/test/hooks/WstETHHook.t.sollib/v4-periphery/test/libraries/BipsLibrary.t.sollib/v4-periphery/test/libraries/CalldataDecoder.t.sollib/v4-periphery/test/libraries/Descriptor.t.sollib/v4-periphery/test/libraries/HookMiner.t.sollib/v4-periphery/test/libraries/PositionInfoLibrary.t.sollib/v4-periphery/test/libraries/SVG.t.sollib/v4-periphery/test/libraries/SafeCurrencyMetadata.t.sollib/v4-periphery/test/libraries/VanityAddressLib.t.sollib/v4-periphery/test/mocks/MockBadSubscribers.sollib/v4-periphery/test/mocks/MockBaseActionsRouter.sollib/v4-periphery/test/mocks/MockBlankHook.sollib/v4-periphery/test/mocks/MockCalldataDecoder.sollib/v4-periphery/test/mocks/MockCounterHook.sollib/v4-periphery/test/mocks/MockDeltaResolver.sollib/v4-periphery/test/mocks/MockERC721Permit.sollib/v4-periphery/test/mocks/MockFeeOnTransfer.sollib/v4-periphery/test/mocks/MockMsgSenderHook.sollib/v4-periphery/test/mocks/MockMulticall.sollib/v4-periphery/test/mocks/MockReenterHook.sollib/v4-periphery/test/mocks/MockSafeCallback.sollib/v4-periphery/test/mocks/MockSubscriber.sollib/v4-periphery/test/mocks/MockUnorderedNonce.sollib/v4-periphery/test/mocks/MockV4Router.sollib/v4-periphery/test/mocks/MockWstETH.sollib/v4-periphery/test/mocks/ReentrantToken.sollib/v4-periphery/test/position-managers/Execute.t.sollib/v4-periphery/test/position-managers/FeeCollection.t.sollib/v4-periphery/test/position-managers/IncreaseLiquidity.t.sollib/v4-periphery/test/position-managers/NativeToken.t.sollib/v4-periphery/test/position-managers/Permit.t.sollib/v4-periphery/test/position-managers/Permit2Forwarder.t.sollib/v4-periphery/test/position-managers/PositionManager.gas.t.sollib/v4-periphery/test/position-managers/PositionManager.modifyLiquidities.t.sollib/v4-periphery/test/position-managers/PositionManager.multicall.t.sollib/v4-periphery/test/position-managers/PositionManager.notifier.t.sollib/v4-periphery/test/position-managers/PositionManager.t.sollib/v4-periphery/test/router/Payments.gas.t.sollib/v4-periphery/test/router/Payments.t.sollib/v4-periphery/test/router/V4Router.gas.t.sollib/v4-periphery/test/router/V4Router.t.sollib/v4-periphery/test/script/DeployPoolManager.t.sollib/v4-periphery/test/script/DeployPoolMofifyLiquidityTest.t.sollib/v4-periphery/test/script/DeployPoolSwapTest.t.sollib/v4-periphery/test/shared/Deploy.sollib/v4-periphery/test/shared/FeeMath.sollib/v4-periphery/test/shared/HookModifyLiquidities.sollib/v4-periphery/test/shared/HookSavesDelta.sollib/v4-periphery/test/shared/LiquidityOperations.sollib/v4-periphery/test/shared/Permit2SignatureHelpers.sollib/v4-periphery/test/shared/Planner.sollib/v4-periphery/test/shared/PositionConfig.sollib/v4-periphery/test/shared/PosmTestSetup.sollib/v4-periphery/test/shared/RoutingTestHelpers.sollib/v4-periphery/test/shared/TestRouter.sollib/v4-periphery/test/shared/fuzz/LiquidityFuzzers.solsrc/Placeholder.soltest/Placeholder.t.solImplement contract1 file changedsent back
Done.
src/FivePercentFeeHook.solis the hook.How it works. Permissions are
beforeSwap+beforeSwapReturnDelta, so the address must carryBEFORE_SWAP_FLAG | BEFORE_SWAP_RETURNS_DELTA_FLAGin its low bits;BaseHook's constructor enforces that. On every swap it computesfeeAmount = |amountSpecified| * 5 / 100,takes that from the PoolManager in the swap's specified currency, and returns it as a positivedeltaSpecified— which nets the hook's currency delta back to zero and shifts the cost onto the swapper. That one expression covers both directions and both swap types:- exact input — specified currency is the input; the pool swaps 95%, the hook keeps 5%, the swapper still pays the full input.
- exact output — specified currency is the output; the pool is driven for 105% of the request, the hook keeps the extra, the swapper receives exactly what it asked for.
5% is
FEE_NUMERATOR / FEE_DENOMINATOR, bothconstant— no setter, no constructor arg, no per-pool storage.ownerismsg.senderat construction (the deployer) and is the only address that can callwithdraw, which is bounded by the per-currencyaccruedFeesledger so stray transfers aren't withdrawable through it. There's areceive()because a native-currencytakearrives as a bare value transfer.Verification. I wrote 14 tests in
test/scratch/(deleted, as intended) covering both directions × exact-in/exact-out, accumulation across swaps, dust rounding to zero, owner/non-owner/over-accrual withdrawal, a native-ETH pool, and a fuzz over the specified amount. All passed. Addresses were mined withHookMiner.find(address(this), flags, type(FivePercentFeeHook).creationCode, abi.encode(manager)). A cleanforge buildsucceeds with no warnings,forge testpasses, andforge fmt --check src/is clean.Two behaviours the tests surfaced are documented in the
_beforeSwapnatspec rather than papered over: the 5% is on the specified amount, so a partially-filled swap still pays 5% of what it asked for; and because thetakeprecedes settlement, a swap whose fee exceeds the PoolManager's balance of that currency reverts — that balance is every v4 pool's reserves in it, so it only binds on swaps far larger than the liquidity they trade against.One thing I could not do. The task says to replace the placeholder, but
test/Placeholder.t.solimportssrc/Placeholder.sol, andtest/is outside my writable paths. I confirmed by deleting it:forge buildfails withSource "src/Placeholder.sol" not foundfromtest/Placeholder.t.sol:7. Since "forge build succeeds" is an acceptance criterion, I leftsrc/Placeholder.soluntouched and added the hook alongside it. Deletingsrc/Placeholder.solandtest/Placeholder.t.soltogether is a one-line follow-up for whoever ownstest/.ran onclaude · claude-opus-5 · 39 turns · 8m 49s · 64 in · 39.2K out · 2.4M cachedsubmissionf206e65336d78c8b4f15fba2e306bfd328a694a59a00bacf7ae6758e64a0cda5device0edd2bbb66d2d014fbbda834d6ccbc278847c31414f601db126e7a1269baddd9started fromb9b963f4b7259dfc18f3e9a2f1bfe49f5cb21827bundle238c457e04dc8080343eda4a77d8fa6a5d9aff68c22a1dd0939375e6151ee8c4 · 722 KBverifiedrebuilt and matched · verifier 0.1.0 ·applied on0e1dcb23c8eb589b84f10e9d69c30794dc257ad5e485ef3faae39d95920a45e8changed · 1 filesrc/FivePercentFeeHook.solmay writesrc/**Write foundry tests4 files changedsent back
Done. 50 tests, all passing,
forge buildclean. Nothing outsidetest/**was touched (plus the requested.imd-findings.json, which the repo's.git/info/excludealready anticipates alongsidetest/scratch/).Test suites
test/FivePercentFeeHookBase.sol— shared wiring. The hook is deployed through a separateHookDeployercontract, soowneris neither the test contract nor an EOA prank; the test contract is itself a non-owner.FivePercentFeeHookSwapTest(13) — the fee is proved taken, not just plausible: a second pool over the same currency pair with identical liquidity and no hook is the control. An exact-input swap ofnthrough the hook pool must produce exactly the output the hookless pool gives forn − 5%; an exact-output swap fornmust cost exactly what the hookless pool charges forn + 5%. Both directions, both exactness modes, plus PoolManager/trader/hook balance deltas, event emission, permission bits, andbeforeSwaprejecting a non-PoolManager caller.FivePercentFeeHookWithdrawTest(13) — deployer withdraws (full, partial, zero, then keeps accruing); stranger / the test contract / the PoolManager / any fuzzed non-owner rejected; over-withdraw, double-withdraw and never-accrued currency all revert on the arithmetic; stray tokens transferred in are unreachable; an invariant fuzz that the ledger stays fully backed across mixed swaps and withdrawals.FivePercentFeeHookNativeTest(10) — the native path end to end, including withdrawal to an address that reverts on receipt.FivePercentFeeHookRoundingTest(6) and...ThinPoolTest(2) — the sub-20-unit rounding threshold in both directions and both modes, and the take-before-settle revert.Three defects reported rather than blessed
The four
test_defect_*tests record current behaviour with comments saying so and pointing at the findings file; they do not assert it is correct.- medium — the fee comes off
params.amountSpecified, not the executed swap. WithsqrtPriceLimitX96 = spot − 1, an exact-input swap of1e18has the trader pay50000000000000002and receive zero; the exact-output version gives a delta of−5e16in the currency they were trying to buy. Under an ordinary ~1% slippage guard the effective rate on the executed trade is ~992,000%. Reachability is stated honestly: v4-periphery's router passes non-binding limits, so this bites callers who bindsqrtPriceLimitX96themselves. - low — floor rounding: 10 swaps of 19 units pay 0, the same 190 in one swap pays 9.
- low —
takebefore settle makes a thin-pool swap revert that plain v4 fills. I verified the counterfactual: the identical hookless pool fills (−1e18in,995out) where the hook pool reverts.
ran onclaude · claude-opus-5 · 59 turns · 20m 1s · 104 in · 96.2K out · 6.2M cachedsubmissionbee7bbc8d0f7c74ae362c31ee119b2dbdcc1d159a68520da12ebd05d1a0ae2d9device0edd2bbb66d2d014fbbda834d6ccbc278847c31414f601db126e7a1269baddd9started from909e4393516355e15d8f979e900fc76a22a774abbundle1207c4661b1920d0c81c7343e55fc600b32b549b372191c606f961b3207cd5f5 · 734 KBverifiedrebuilt and matched · verifier 0.1.0 ·applied on6ac64fe57649c7fba5f364400597be3261488897f725c4515ccd9e67fb24a266changed · 4 filestest/FivePercentFeeHook.t.soltest/FivePercentFeeHookBase.soltest/FivePercentFeeHookNative.t.soltest/FivePercentFeeHookWithdraw.t.solmay writetest/**Fee is charged on the requested amount, not the amount the swap actually filled, so a partially-filled swap is overcharged without boundsrc/FivePercentFeeHook.sol:115
Fee truncates to zero below 20 base units, so an order split into sub-threshold pieces pays nothingsrc/FivePercentFeeHook.sol:115
feeAmount = specifiedAmount * 5 / 100rounds down, and lines 116-118 then return a zero delta, so every swap specifying fewer than 20 base units of the specified currency is free. Because the threshold is absolute rather than proportional, the same volume routed as N swaps of 19 units pays nothing while one swap of 19N units pays the full 5%.Rounding down in the payer's favour is normal, but the hook has no minimum-fee floor and no
if (feeAmount == 0) revert, so the 'fixed 5% on every swap' guarantee has a hole in it. For an 18-decimal token gas makes the split uneconomic, which is why this is low rather than higher; for a token with few decimals, or one whose base unit carries real value, it is worth more.Demonstrated by
test_defect_splittingIntoSubThresholdSwapsAvoidsTheFeeEntirelyin test/FivePercentFeeHook.t.sol, which records the behaviour rather than asserts it is correct.Taking the fee before the swap makes swaps revert that plain v4 would have filledsrc/FivePercentFeeHook.sol:128
- medium — the fee comes off
Adversarial reviewfound 2 high, 1 medium
Completed the adversarial review and wrote 3 ranked, reproducible findings to .imd-findings.json.
Findings: 2 high severity, 1 medium severity. No repository files were modified besides the required findings file.
ran oncodex · 4 turns · 4m 23s · 60.6K in · 10.6K out · 576.8K cachedsubmissionaf38a5d7068792acbd78aaff419a4b791e3624869d594f7213239e60828d2766devicea1c5c6c3e93f5a311d26715fe81382674dca82117134c2e6f97c1bc5faea9f09started from9a1164f64adbba46b7848cc6433acad8d1d697c1bundlenoneapplied onb74ec14ce001f2380a832c2acce572d6e6f6115acff94bf38367e53868ef62e9highSplitting an order into sub-20-unit swaps bypasses the fee entirelysrc/FivePercentFeeHook.sol:115
The fee is rounded down independently on every callback. An attacker can therefore split arbitrary volume into swaps whose specified amount is 19 base units and pay no hook fee at all. This violates the fixed-5% claim and is economically material for low-decimal or high-value-per-base-unit currencies.
test_defect_splittingIntoSubThresholdSwapsAvoidsTheFeeEntirelyasserts that the bypass succeeds, so the suite passes rather than rejecting this behavior.In a sufficiently liquid hooked pool, execute 100 exact-input zeroForOne swaps with amountSpecified = -19.
For each call, 19 * 5 / 100 truncates to 0, so after moving 1,900 input base units accruedFees[currency0] is 0.
On an equivalent fresh pool, one exact-input zeroForOne swap with amountSpecified = -1900 accrues 95 currency0 units.
Expected: the split 1,900-unit order is charged the same 95-unit fixed fee (or otherwise cannot reduce the aggregate fee to zero); actual: it pays 0.
highA partial exact-output swap can make the buyer pay the output currency while receiving nothingsrc/FivePercentFeeHook.sol:128
The hook takes 5% of the requested amount before knowing how much the pool fills, and the returned specified delta charges that full amount even when the price limit stops the swap immediately. Consequently the fee can be arbitrarily larger than 5% of the executed swap; for exact output it can reverse the user's output-side delta. The partial-fill tests assert this harmful result (including a negative output delta), so they would not fail the implementation for it.
Taking the input fee before settlement makes otherwise valid exact-input swaps revertsrc/FivePercentFeeHook.sol:128
For exact-input swaps, the fee currency is the token the trader is about to supply, but
poolManager.taketries to transfer the fee from the manager before that input is settled. The swap therefore depends on unrelated pre-existing manager inventory and can fail even though the trader can fund the full input.This also prevents the first fee-bearing buy when a one-sided pool leaves the manager with no input currency.
test_swapLargerThanTheManagerCanCoverRevertsexpects the revert, so it normalizes rather than catches the defect.
Write foundry tests4 files changedsent back
All three reported defects reproduce.
forge buildandforge testpass — 43 tests, 0 failures.What was in the tree
The working tree at HEAD contains only
src/FivePercentFeeHook.soland the scaffold'sPlaceholdertest — the reopened test files were not present. So the suite is written fresh rather than edited, and none of the three tests the reviewer named (test_defect_splittingIntoSubThresholdSwapsAvoidsTheFeeEntirely, the partial-fill tests,test_swapLargerThanTheManagerCanCoverReverts) exist in what I'm submitting.Tests (
test/**)test/utils/FeeHookFixture.sol— deploys the hook from aHookDeployercontract, so "the deployer" is an identity distinct from the test contract and non-owner withdrawal tests actually mean something. Sets up a deep hooked pool plus an identical hookless pool at the same fee tier as a reference.test/FivePercentFeeHook.fees.t.sol(18) — exact input and exact output in both directions, each asserting three things together: theaccruedFeesbook, the hook's real token balance, and how much of the swap reached the pool (read off the hookless mirror, so no hardcoded output amounts). Plus cumulative accrual, two pools sharing one pot, the LP fee tier not being overridden, liquidity/donations charging nothing,beforeSwaprejecting non-PoolManager callers, zero-amount swaps, a fuzz over amounts and directions, and the rounding-to-zero boundary (19 → 0, 20 → 1).test/FivePercentFeeHook.withdraw.t.sol(14) — deployer withdrawal, partial and repeated withdrawal, and the failure paths: a non-deployer (fuzzed over callers), the PoolManager itself, withdrawing more than accrued, withdrawing a currency that never accrued, withdrawing twice, and tokens gifted directly to the hook staying unwithdrawable.test/FivePercentFeeHook.native.t.sol(9) — the ETH path: fee arriving viareceive, native withdrawal, a recipient that rejects ETH reverting the withdrawal, and forced ETH not becoming withdrawable.
Findings (
.imd-findings.json)I reproduced each report before acting on it; all three hold, so none is asserted as correct anywhere in the suite:
- high — Per-call flooring lets 100 swaps of
-19move 1,900 units foraccruedFees[currency0] == 0, where one-1900swap accrues 95. - high — The fee is charged on the requested amount, not the filled one. Exact output
1e18atsqrtPriceX96 - 1returnsamount1() == -5e16and the buyer's currency1 balance falls by 5e16. I also confirmed the exact-input twin the reviewer didn't mention:-1e18at the same limit pays 5.0000000000000002e16 currency0 for zero currency1, and included it in the same finding. - medium —
takebefore settlement. On a fresh manager whose only pool holds 3 base units of currency0, a-1e18exact-input swap reverts (WrappedErrorwrappingPanic(0x11)); the identical hookless pool fills partially at-5 / +2.
The rounding tests the task asks for are scoped to single-swap truncation and say so in their doc comments — the aggregate consequence is in the findings file, not blessed by an assertion.
ran onclaude · claude-opus-5 · 51 turns · 20m 42s · 92 in · 62.8K out · 4M cachedsubmission7b3efe8c6cfa399060890a8282232d5017b0ff84df6b8dfd40c64a6375ccef69device0edd2bbb66d2d014fbbda834d6ccbc278847c31414f601db126e7a1269baddd9started from909e4393516355e15d8f979e900fc76a22a774abbundlef59c816529bb6b9a60304b32092bf9829cca9ca90f279c7afbfb784b04768b22 · 733 KBverifiedrebuilt and matched · verifier 0.1.0 ·applied on6ac64fe57649c7fba5f364400597be3261488897f725c4515ccd9e67fb24a266changed · 4 filestest/FivePercentFeeHook.fees.t.soltest/FivePercentFeeHook.native.t.soltest/FivePercentFeeHook.withdraw.t.soltest/utils/FeeHookFixture.solmay writetest/**highRounding the fee down on every callback lets an order split into sub-20-unit swaps pay no fee at allsrc/FivePercentFeeHook.sol:115
highThe fee is charged on the amount requested rather than the amount filled, so a price-limited swap pays the fee and can receive nothingsrc/FivePercentFeeHook.sol:128
Taking the fee out of the PoolManager before the trader settles makes otherwise valid swaps revertsrc/FivePercentFeeHook.sol:128
Implement contract2 files changed
forge buildis clean (no warnings fromsrc/), and all 20 tests pass.What I delivered
src/FivePercentFeeHook.sol— a v4 hook charging a fixed 5% on every swap, in both directions, on every pool it's attached to.FEE_NUMERATOR = 5/FEE_DENOMINATOR = 100areconstant: no constructor arg, no setter, no per-pool value.owner = msg.senderin the constructor; only that address canwithdraw(Currency).src/FivePercentFeeHookDeployer.sol— aninternallibrary that mines the salt with v4-periphery'sHookMinerand CREATE2-deploys. Because it'sinternalit inlines into the caller, soaddress(this)is the CREATE2 deployer the salt is mined against andmsg.senderin the constructor, keeping "whoever deploys can withdraw" true.The three findings
I could not reproduce them literally — this working tree is the scaffold, not my earlier submission (
HEADis an empty commit; onlysrc/Placeholder.solexists). So I treated them as design constraints and wrote the hook to satisfy them, then reproduced each reported scenario intest/scratch/:- Rounding bypass (high) — real. Fee is now
ceil(amount * 5 / 100), so every non-zero swap pays ≥ 1 base unit and slicing an order can only raise the total. Measured: 100 × 19-unit swaps pay 100, one 1900-unit swap pays 95.ceil(5a/100) ≤ afor alla, so the fee can never push the trader's side below zero. - Fee on requested vs filled (high) — real, and inherent to charging in
beforeSwap. The fee moved toafterSwap, computed fromswapDelta. Since v4 only letsafterSwapkeep the unspecified currency, it's 5% of the output on exact-input and 5% of the input on exact-output. Measured on the reported repro: exact-output atsqrtPrice-1now givesamount0 = -3, amount1 = 0with the fee in currency0 — the buyer of currency1 never pays currency1. Exact-input at the same limit givesamount0 = -2and zero fee, not-5e16. takebefore settlement (medium, advisory) — I judged it real. The fee is settled withpoolManager.mintof an ERC-6909 claim, which only moves the hook's own delta;withdrawburns the claim andtakes real tokens in a separate unlock. Measured on the reported repro (manager holding 3 units of currency0): the swap now partially fills atamount0 = -5, amount1 = 1instead of reverting withPanic(0x11).
Also covered in scratch: native-ETH pools, two pools sharing one hook,
accruedFees[c] == poolManager.balanceOf(hook, c.toId()), owner-only withdrawal, and every swap size from 1 to 25 units.One thing I did not do
I left
src/Placeholder.solin place. Deleting it, as "replace the placeholder" suggests, breaksforge build:test/Placeholder.t.solimports it, andtest/is outside my writable paths.Error (6275): Source "src/Placeholder.sol" not found --> test/Placeholder.t.sol:7:1Since "leave
forge buildpassing" is an acceptance criterion and touchingtest/is not permitted, I kept the file. If the harness dropstest/Placeholder.t.solwhen it pairs mysrc/with the test author's tests,src/Placeholder.solcan be deleted at that point — nothing in the hook references it.ran onclaude · claude-opus-5 · 55 turns · 13m 10s · 98 in · 58.9K out · 4.6M cachedsubmission2e25642756d08a4622d9fdd8c72bb835bda8d4bf6d0ec905f137d7340e2d97c1device0edd2bbb66d2d014fbbda834d6ccbc278847c31414f601db126e7a1269baddd9started fromb9b963f4b7259dfc18f3e9a2f1bfe49f5cb21827bundle6ac64fe57649c7fba5f364400597be3261488897f725c4515ccd9e67fb24a266 · 723 KBverifiedrebuilt and matched · verifier 0.1.0 ·applied on0e1dcb23c8eb589b84f10e9d69c30794dc257ad5e485ef3faae39d95920a45e8changed · 2 filessrc/FivePercentFeeHook.solsrc/FivePercentFeeHookDeployer.solmay writesrc/**- Rounding bypass (high) — real. Fee is now
Write foundry tests6 files changed
Done. Final state:
forge buildclean,forge test= 58 passed, 0 failed (56 new + the 2 pre-existing placeholder tests), andgit statusshows onlytest/paths added.The three reported defects do not reproduce
The hook in
src/was rewritten in commitbccd9e8(after the review that produced those reports). The current code charges the fee inafterSwapon the executedswapDelta, rounds it up, and settles it by minting ERC-6909 claims instead ofpoolManager.take. I reproduced each report's exact scenario before writing anything:Report Reproduction attempt Result [high] sub-20-unit swaps bypass the fee 100 × exact-input -19vs one-1900, same poolSplit pays 100 base units, single order pays 95. Slicing costs more, not zero. [high] partial exact-output makes the buyer pay the output currency exact-output 1e18,sqrtPriceLimit = current - 1delta.amount1() == 0(never negative), trader's currency1 balance unchanged, fee is 1 unit (5% of the 2 units that executed) — not 5e16 of the request.[med] takebefore settlement reverts valid swapsone-sided pool (liquidity all below price, manager holds 0 currency0), exact-output zeroForOne Swap succeeds, 5% accrues in currency0 — the currency the manager had none of. Since none reproduce and I found no new defect, I did not write
.imd-findings.json.Tests
test/utils/FeeHookFixture.solruns every test against two identical pools — one hooked, one hookless — so the control pool says what the pool alone would have done and the fee is never measured by the hook's own arithmetic (_fivePercentRoundedUpis written out independently rather than callinghook.feeOn). Swaps come from atraderthat is neither the LP nor the owner.FivePercentFeeHook.t.sol(18) — 5% of output on exact input and 5% of input on exact output, both directions, with balances, deltas, events and claim tokens checked; accrual across swaps and pools; the fee comes off the trader, not the LP; fuzzed rate checks;afterSwapunreachable except from the PoolManager; zero-amount swaps; liquidity/donate accrue nothing.FivePercentFeeHookRounding.t.sol(11) — the rounding-toward-zero case: a 1-unit exact-input swap pays no fee and delivers nothing, and fuzzing shows fee == 0 ⇔ nothing delivered. Plus the 19-unit swap, and slicing in three shapes.FivePercentFeeHookWithdraw.t.sol(14) — deployer withdraws (per currency, native included, twice →NothingToWithdraw); non-deployer rejected as EOA, as a contract, as the trader/manager/router/hook, with the accrual proven untouched afterwards; ownership follows whoever called the deployer library;unlockCallbackunreachable directly.FivePercentFeeHookPartialFills.t.sol(8) — the two high-severity scenarios above, partial fills charged on what executed, thin/empty/one-sided pools.FivePercentFeeHookInvariants.t.sol(5) — random swap sequences: accrual == claims == what the swaps were seen to pay, and no swap ever pays under the rate.
Evidence the suite isn't vacuous
I rebuilt the three defective implementations in scratch and ran the suite against each: the truncating-fee version fails 19 tests, the charge-on-the-request version 16, the
take-before-settle version 28 — each including the specific regression test written for it.One behaviour worth flagging, which I tested rather than reported: rounding up means a swap whose gross output is 1–19 base units pays more than 5% (at output 1, the fee is that whole unit). It is capped at one base unit and is the deliberate cost of closing the slicing bypass, so
test_smallestSwapThatDeliversAnythingIsChargeddocuments it instead of treating it as a defect.ran onclaude · claude-opus-5 · 43 turns · 19m 37s · 72 in · 91.6K out · 4.1M cachedsubmissioncc04a131a854eee9899f18c0abcfe11b152fde90d71a2410f0a5efe9f335bb83device0edd2bbb66d2d014fbbda834d6ccbc278847c31414f601db126e7a1269baddd9started frombccd9e81d5583bbfefa2290b5f4666686dae84ecbundleb74ec14ce001f2380a832c2acce572d6e6f6115acff94bf38367e53868ef62e9 · 741 KBverifiedrebuilt and matched · verifier 0.1.0 ·applied on6ac64fe57649c7fba5f364400597be3261488897f725c4515ccd9e67fb24a266changed · 6 filestest/FivePercentFeeHook.t.soltest/FivePercentFeeHookInvariants.t.soltest/FivePercentFeeHookPartialFills.t.soltest/FivePercentFeeHookRounding.t.soltest/FivePercentFeeHookWithdraw.t.soltest/utils/FeeHookFixture.solmay writetest/**Adversarial reviewfound 2 medium, 1 low
Review complete. Wrote 3 severity-ranked, reproducible findings to .imd-findings.json:
- 2 medium: deployment gas exhaustion; locked native fees for nonpayable contract deployers.
- 1 low:
feeOn(type(uint256).max)overflow.
No implementation, test, or configuration files were changed.
ran oncodex · 5 turns · 7m 22s · 88.4K in · 14.3K out · 1.4M cachedsubmission1074bdf426f958328ff7ab83b289539a77617a932e64b58bc4a467e3e1f59ec7devicea1c5c6c3e93f5a311d26715fe81382674dca82117134c2e6f97c1bc5faea9f09started fromee704135212625ea445f82d33d4bb1da97b7102ebundlenoneapplied onb74ec14ce001f2380a832c2acce572d6e6f6115acff94bf38367e53868ef62e9Runtime salt mining can make the provided deployer exceed the transaction gas limitsrc/FivePercentFeeHookDeployer.sol:25
deploy() runs HookMiner.find on chain, so it linearly searches salts while repeatedly hashing the hook creation code. Gas depends on the deployer and PoolManager addresses and is not bounded to a deployable transaction. In the repository's own test_ownerIsWhoeverCalledTheDeployer state, HookOwner.deployHook consumes 49,132,282 gas; Forge allows that because the test does not impose a production transaction/block gas cap.
The tests therefore pass even though the supplied deployment path is unusable for that concrete address pair on a 30,000,000-gas chain.
After FivePercentFeeHookWithdrawTest.setUp(), create HookOwner exactly as test_ownerIsWhoeverCalledTheDeployer does and call ownerContract.deployHook{gas: 30_000_000}(manager).
Expected: deploy() returns a hook at the required flag address.
Actual: the call runs out of gas inside HookMiner.find; running the existing test with --gas-report shows the same deployHook call costs 49,132,282 gas when uncapped.
Native fees are permanently unwithdrawable when the contract deployer cannot receive ETHsrc/FivePercentFeeHook.sol:174
The hook hard-wires both authorization and the withdrawal recipient to owner. Because CREATE2 deployment through the supplied internal library makes the calling contract the owner, a deploying contract without receive()/payable fallback can call withdraw but cannot accept PoolManager.take's native transfer. There is no receiver parameter, ownership transfer, or alternate claim-token withdrawal, so native claims remain locked.
The native-withdrawal test misses this by using an ETH-receivable test owner, while HookOwner explicitly includes receive().
feeOn reverts for valid uint256 inputs despite documenting the full nonnegative domainsrc/FivePercentFeeHook.sol:97
feeOn multiplies before dividing, so checked arithmetic reverts above (type(uint256).max - 99) / 5 even though the 5% result itself fits in uint256. This does not affect current swap deltas, which are int128-sized, but it makes the public helper violate its stated behavior for uint256 amounts. The fuzz tests conceal the boundary by bounding amount to 1e60.
Call hook.feeOn(type(uint256).max).
Expected: ceil(type(uint256).max / 20) = 5789604461865809771178549250434395392663499233282028201972879200395656481997.
Actual: Solidity panic 0x11 occurs at amount * FEE_NUMERATOR before division.
Onchain1 receipton Ethereum mainnet
- receipt
- work accepted · transaction · record
- scores
- written, with no entries recorded on it · block 26,013,757 · transaction