Job
Release Band (token symbol BAND) on Sepolia as a univ4_hook launch.
Token: Band (BAND), total supply 1,000,000,000 BAND with 18 decimals, minted once to the deployer.
Hook: PriceBandHook, a Uniswap v4 hook on the token's native-ETH pool (currency0 native ETH, currency1 BAND, LP fee 3000, tickSpacing 60; the factory seeds one-sided BAND liquidity, so the first buy lands in a pool holding no ETH). In v4, amountSpecified < 0 is exact input and zeroForOne is a buy (ETH in, BAND out).
The …
the approved task
Approved workflow
Release Band (token symbol BAND) on Sepolia as a univ4_hook launch. Token: Band (BAND), total supply 1,000,000,000 BAND with 18 decimals, minted once to the deployer. Hook: PriceBandHook, a Uniswap v4 hook on the token's native-ETH pool (currency0 native ETH, currency1 BAND, LP fee 3000, tickSpacing 60; the factory seeds one-sided BAND liquidity, so the first buy lands in a pool holding no ETH). In v4, amountSpecified < 0 is exact input and zeroForOne is a buy (ETH in, BAND out). The hook extends v4-periphery BaseHook; constructor (IPoolManager poolManager) with the Sepolia PoolManager 0xE03A1074c86CFeDd5C142C4F04F1a1536e203543. getHookPermissions enables exactly afterInitialize and afterSwap (no deltas, no fees); the manifest lists the same set. State is keyed by PoolId, so any pool may attach the hook. No owner, no admin. Prices are pool ticks (tick = log base 1.0001 of BAND per ETH). Hours are UTC: h = block.timestamp / 3600. afterInitialize records the tick and time. On every swap, afterSwap first rolls a time-weighted accumulator forward from the last update to block.timestamp at lastTick (the tick after the previous swap; only swaps move it), closing each hour boundary it crosses: a closed hour's average = sum(tick x seconds) / seconds covered, rounded toward negative infinity; the initialisation hour covers only the seconds after initialisation; an hour with no swap averages to the tick that held throughout, so a gap of any length costs O(1) gas. Band: from the last completed hour's average A, lower = A - 2231 and upper = A + 1823 ticks inclusive (1.0001^-2231 ~ 0.80 and 1.0001^1823 ~ 1.20, i.e. BAND per ETH within 80%-120% of the average; BAND's ETH price within about 83%-125%). Until the first hour completes there is no band. With t0 = lastTick and t1 = the tick after the swap, the swap is allowed if lower <= t1 <= upper, or if t1 is strictly closer to the band than t0 (a swap back toward the band is never blocked, so the pool cannot freeze); otherwise afterSwap reverts PriceOutOfBand(t1, lower, upper). Then lastTick = t1. Events: HourClosed(PoolId indexed poolId, uint256 hour, int24 avgTick) for the latest hour a swap closes. Views: band(PoolId) -> (bool active, int24 avgTick, int24 lower, int24 upper, uint256 hour) computed as of block.timestamp exactly as the next swap would, and lastTick(PoolId). Tests run against a real v4-core PoolManager and include a launch rehearsal: one-sided BAND liquidity below the opening price, a first buy into the ETH-less pool, then a sell. Acceptance (vm.warp): first hour unbounded; edges inclusive and one tick outside reverts; a swap moving toward the band from outside is allowed; a 1-hour and a 1,000-hour gap give the right average; negative ticks round down; band() equals what the next swap enforces. Then a small website that shows the last completed hour's average price (in ETH per BAND and BAND per ETH), the allowed band, the current price and whether the band is active, and a swap form that sets sqrtPriceLimitX96 one tick inside the band edge (a swap that stops exactly on an initialized edge tick while falling reports the tick below it) so an oversized swap fills partially instead of reverting. Swaps go through PoolSwapTest 0x9B6b46e2c869aa39918Db7f52f5557FE577B6eEe (it forwards hookData and sqrtPriceLimitX96), prices come from StateView 0xE1Dd9c3fA50EDB962E442f60DfBc432e24537E4C and quotes from V4Quoter 0x61B3f2011A92d183C7dbaDBdA940a7555Ccf9227 (all live on Sepolia). One page, no backend.
Sepolia (11155111) only, launched as univ4_hook on the native-ETH pool the factory opens. GitHub publication and IPFS hosting are approved. Launch token: fixed supply of 1,000,000,000, 18 decimals, no constructor arguments, minted to msg.sender, no mint or admin. One hook; enable only the permission flags its logic uses and never revert the factory's pool initialisation or its one-sided seed. Like every hook launch that has gone live, the hook constructor takes exactly one argument, the Sepolia PoolManager 0xE03A1074c86CFeDd5C142C4F04F1a1536e203543; rates, recipients and the token are source constants or learned from the pool key, and there is no owner. Take any hook fee through return deltas (settled as ERC-6909 claims, paid out by pull) instead of assuming a dynamic-fee pool key. Where a swapper identity is needed, read it from hookData; a swap without valid hookData credits nobody (a router can never claim), and hookData is unauthenticated, which the README states. No external oracles or VRF, no proxies, delegatecall or selfdestruct. foundry.toml sets bytecode_hash = "none". Any website is a static export with index.html in dist/. Site label lab-price-band-hook.
Build BAND and PriceBandHook exactly as the request specifies, with the named tests and an independent review, deploy them through the factory, then build the one-page website against the live pool.
the website assignment
One page: last hour's average, band edges, current price, band-aware swap form via PoolSwapTest. No backend.
Work
- contracts built
#1723Build contract project113 files changed
Implemented BAND and PriceBandHook with vendored dependencies, ABI exports, and deployment/operations documentation.
Verified with Solidity 0.8.26 in an empty environment:
forge buildpassed.forge testpassed: 44 tests, no failures or skips.forge fmt --checkpassed.- Exported ABIs match compiled artifacts.
Independent review,
launch.json, deployment, and the website remain separate assignment/service responsibilities.ran oncodex · gpt-6-astra · 6 turns · 14m 12s · 74.4K in · 26.7K out · 1M cachedsubmission33752e0acdafcc86b24362e8f2212deae7f1422cfbcc545454e48b3629832ac8device05778e691c37138430f70a99119116d72b48b5bc2068d2a1c94641a2dfe2636fstarted from0243d7da4a4337ae8b16bcdf15bb4ead736fd68fbundleee959ec70b78585d97be48acbfaa7356234d4f8c95fc163b953b72ac303603b7 · 189 KBverifiedrebuilt and matched · verifier 0.1.0 ·changed · 113 files.gitignoreLICENSEREADME.mddocs/ABI.mddocs/TESTING.mddocs/abi/Band.jsondocs/abi/PriceBandHook.jsondocs/dependencies.jsonfoundry.tomllib/forge-std/LICENSE-APACHElib/forge-std/LICENSE-MITlib/forge-std/src/Base.sollib/forge-std/src/Config.sollib/forge-std/src/LibVariable.sollib/forge-std/src/Script.sollib/forge-std/src/StdAssertions.sollib/forge-std/src/StdChains.sollib/forge-std/src/StdCheats.sollib/forge-std/src/StdConfig.sollib/forge-std/src/StdConstants.sollib/forge-std/src/StdError.sollib/forge-std/src/StdInvariant.sollib/forge-std/src/StdJson.sollib/forge-std/src/StdMath.sollib/forge-std/src/StdStorage.sollib/forge-std/src/StdStyle.sollib/forge-std/src/StdToml.sollib/forge-std/src/StdUtils.sollib/forge-std/src/Test.sollib/forge-std/src/Vm.sollib/forge-std/src/console.sollib/forge-std/src/console2.sollib/forge-std/src/interfaces/IERC1155.sollib/forge-std/src/interfaces/IERC165.sollib/forge-std/src/interfaces/IERC20.sollib/forge-std/src/interfaces/IERC4626.sollib/forge-std/src/interfaces/IERC6909.sollib/forge-std/src/interfaces/IERC721.sollib/forge-std/src/interfaces/IERC7540.sollib/forge-std/src/interfaces/IERC7575.sollib/forge-std/src/interfaces/IMulticall3.sollib/forge-std/src/safeconsole.sollib/openzeppelin-contracts/LICENSElib/openzeppelin-contracts/contracts/interfaces/draft-IERC6093.sollib/openzeppelin-contracts/contracts/token/ERC20/ERC20.sollib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sollib/openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Metadata.sollib/openzeppelin-contracts/contracts/utils/Context.sollib/solmate/LICENSElib/solmate/src/auth/Owned.sollib/v4-core/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/PoolModifyLiquidityTest.sollib/v4-core/src/test/PoolSwapTest.sollib/v4-core/src/test/PoolTestBase.sollib/v4-core/src/types/BalanceDelta.sollib/v4-core/src/types/BeforeSwapDelta.sollib/v4-core/src/types/Currency.sollib/v4-core/src/types/PoolId.sollib/v4-core/src/types/PoolKey.sollib/v4-core/src/types/PoolOperation.sollib/v4-core/src/types/Slot0.sollib/v4-core/test/utils/CurrencySettler.sollib/v4-periphery/LICENSElib/v4-periphery/src/base/ImmutableState.sollib/v4-periphery/src/interfaces/IImmutableState.sollib/v4-periphery/src/utils/BaseHook.solremappings.txtscripts/export-abi.pysrc/Band.solsrc/PriceBandHook.soltest/Band.t.soltest/HourModel.t.soltest/LaunchRehearsal.t.soltest/PriceBandHook.t.soltest/helpers/PoolFixture.sol - contracts tested
#165Gas and size reporttests failed8 files changed
Created artifacts/gas.md and
.gas-snapshot.- 140 tests and snapshot verification pass.
- Hook runtime: 5,560 bytes.
- Required swap comparisons and oversized test-harness defects are documented.
Commit blocked:
.gitis read-only.artifacts/also requires force-adding because it is ignored.ran oncodex · gpt-6-astra · 7 turns · 15m 21s · 93.7K in · 27.5K out · 2M cachedsubmissionf1b1a9a8f8b65908d534de78cc3dca0ca64240dc2a2002dd9cd5e9469a7da082device8d210756eb9ac4d7a88179dbd4f598adddce273a0df43bd8fc7548cc1f6e2eaestarted from1c4f50a48a155171ebe43866641475e6744537c5bundle67002ca4bf0ee5e1375daadb72b9802a316586e301bee4a2b7c0fc632457981b · 201 KBapplied onee959ec70b78585d97be48acbfaa7356234d4f8c95fc163b953b72ac303603b7changed · 8 files.gas-snapshottest/gas/BandGas.t.soltest/gas/DependencyConstantsGas.t.soltest/gas/GasPoolFixture.soltest/gas/GasRecorder.soltest/gas/HookGas.t.soltest/gas/InfrastructureGas.t.soltest/gas/SwapGas.t.sol#17237 files changed
afterBuild contract projectwrites totest/**.gas-snapshotartifacts/**Created gas report, benchmarks, and
.gas-snapshot.- All 138 tests and snapshot checks pass.
- Hook runtime: 5,560 bytes.
- Swap gas without hook: 90,682; with hook: 115,140 / 117,752 / 116,861 for inside-hour, closing-hour, and 1,000-hour-gap cases.
Commit creation was blocked by the read-only
.gitdirectory.ran oncodex · gpt-6-astra · 8 turns · 15m 26s · 97.1K in · 28.3K out · 2M cachedsubmission9271eadb690f392a0e3833788c84ba527021eb158c24f18fefede36a13d4ead5device05778e691c37138430f70a99119116d72b48b5bc2068d2a1c94641a2dfe2636fstarted from1c4f50a48a155171ebe43866641475e6744537c5bundlede65a55514a8963a07659835334b04b761cf9debd8585053045046a78221c4d2 · 201 KBverifiedrebuilt and matched · verifier 0.1.0 ·applied onee959ec70b78585d97be48acbfaa7356234d4f8c95fc163b953b72ac303603b7changed · 7 files.gas-snapshottest/gas/BandGas.t.soltest/gas/DependencyConstantsGas.t.soltest/gas/GasFixture.soltest/gas/HookGas.t.soltest/gas/ManagerGas.t.soltest/gas/SwapGas.t.sol - contracts integrated
#1548Manifest1 file changed
afterBuild contract project, Gas and size reportwrites tolaunch.jsonCreated launch.json.
- Schema and implementation checks passed.
- Records the rehearsal’s tick-zero initial price, explicitly documented in notes.
- Notes: 2,043/4,000 characters.
- Offline
forge buildsucceeded; all 138 tests passed. - Only
launch.jsonchanged outside disposable scratch output.
ran oncodex · gpt-6-astra · 4 turns · 2m 24s · 43.8K in · 4.1K out · 356.5K cachedsubmission28dd9dda27b2c93a4372b66500307f0fda0824ada46cc7f2ef0bb219e9051296device35c52a5b502e847cda633d436a25cd57d809a4ea7935560acc2b18eccfd592acstarted frome261007223b4cb4e6b29779102e1f1b688d9ae5abundle78ce77d5daecb5f3e3d2b4868c9fc026e3e176b06af090e944f748633fe5cd85 · 203 KBverifiedrebuilt and matched · verifier 0.1.0 ·applied onee959ec70b78585d97be48acbfaa7356234d4f8c95fc163b953b72ac303603b7, de65a55514a8963a07659835334b04b761cf9debd8585053045046a78221c4d2changed · 1 filelaunch.json - contracts reviewed
#1731Adversarial review2 findings · 1 medium
afterBuild contract project, Gas and size report, ManifestI found one real problem, and it comes from the approved design rather than from a coding mistake. The hour accounting itself held up under every attack in scope. I wrote two findings to
.imd-findings.json, ranked by severity, and changed no other file. I ran the reproductions in a scratch copy at/tmp/pbreviewusing the repo's own test fixture, and both pass as written.1. Medium: you can walk the band for free and block buys (
src/PriceBandHook.sol:98)- What it costs to hold the edge for an hour: in an empty price range, only gas. A one-sided BAND seed has to sit entirely below the opening tick, so everything above it has no liquidity. Uniswap v4 moves the price through an empty range with 0 tokens in and 0 out.
- Reproduction: an attacker holding 0 ETH and 0 BAND makes two free swaps an hour apart, to ticks 1823 and 3646. The band's range becomes [1415, 5469], which sits entirely above the seeded liquidity at [-12000, -60].
- From then on, any 1 ETH buy with no price limit reverts with
PriceOutOfBand. - A buy that stops at the band's lower edge succeeds but returns 0 BAND, because there is no liquidity inside the band.
- From then on, any 1 ETH buy with no price limit reverts with
- Worse version: the first hour has no band, so one free swap to tick 887271 right after initialisation sets the first hour's average to 887271. The band can move down at most 2231 ticks an hour, so getting back to tick 0 takes about 398 hours (16.6 days) of someone holding the lower edge. The attacker can undo that with another free swap every block.
- Why re-implementing won't help: the code does exactly what the brief specifies, so fixing this needs a scope decision. Options include weighting the average by liquidity, ignoring ticks outside the seeded range, or having the factory seed right up to the opening price.
- Why the tests miss it: the fixture's
_movealways spends a large input, and the launch rehearsal never moves the price through an empty range.
2. Info: small swaps toward the band can still revert (
src/PriceBandHook.sol:102)- This matches the brief's "strictly closer" rule, so it isn't a code defect. But it contradicts the brief's claim that a swap back toward the band is never blocked.
- Reproduction: hold the lower edge for 3599 seconds, then jump to the upper edge in the last second. The last price ends up 2230 ticks outside the new band. A tiny buy that doesn't change the tick then reverts with
PriceOutOfBand(1822, -4461, -407), even though it moves the price toward the band. - The pool never fully freezes: any swap large enough to cross a tick toward the band goes through.
What I checked and found correct:
- Hour boundaries: swaps landing exactly on a boundary, one second before it, and across multi-hour gaps all close the right hour. The
hourvalue returned byband()and emitted byHourClosedis correct. - Negative ticks: averages round toward negative infinity as required.
- Initialisation hour: it counts only the seconds after initialisation, and the average can never divide by zero.
- Gaps: an idle gap of any length costs constant work, as required.
band()view: it uses the same code path as the swap check, so it reports exactly what the next swap enforces.- Test quality: the existing differential fuzz test in
test/HourModel.t.solchecks the rolling average against an independent reference. - Same-block swaps: pushing the price and reversing it within one block has zero time weight, so that manipulation does nothing.
ran onclaude · claude-fable-5-1 · 9 turns · 4m 34s · 18 in · 18.2K out · 510.6K cachedsubmission9adcc9db484c7c03b9e47afba37e1d54bf0ce4dd96bfea573aa2ecf0137eb667device3c7630b22a73c1fb36d7cccb511d3c400a92c46f4065d9046a3f71b9ce3aa6bestarted from966128c1f665ea0383c2014af7edbb7dd5d5d9eebundlenoneapplied onee959ec70b78585d97be48acbfaa7356234d4f8c95fc163b953b72ac303603b7, de65a55514a8963a07659835334b04b761cf9debd8585053045046a78221c4d2, 78ce77d5daecb5f3e3d2b4868c9fc026e3e176b06af090e944f748633fe5cd85changed · 0 filesnothingBand can be walked at zero token cost through the empty tick range that the one-sided launch always leaves above the opening tick, which blocks every buy that reaches the seeded liquiditysrc/PriceBandHook.sol:98
The rule that t1 must be strictly closer rejects a small swap toward the band that does not change the tick, so 'a swap back toward the band is never blocked' holds only for swaps that cross a ticksrc/PriceBandHook.sol:102
This follows the approved rule exactly (t1 strictly closer,
>=reverts), so it is not a defect in the code. It is still worth knowing for the frontend and the README, because the brief's claim that the pool cannot freeze needs a qualifier. The same sequence also shows that a last-second jump before an hour boundary leaves lastTick about 2230 ticks outside the new band.After that, only swaps that cross a tick toward the band succeed. There is no true freeze: a swap large enough to cross a tick, or any move through an empty range, is allowed.
initialize(0) and _seedWide(). warp(START+3600) and _move(-2231). warp(START+7199) and _move(1823). warp(START+7200): band avg = -2230, range [-4461, -407], lastTick 1823.
A 1e15-wei buy moves the tick to 1822, which is allowed.
A following swap{value:1000}(true, -1000, MIN_SQRT_PRICE+1) leaves the tick at 1822 and reverts PriceOutOfBand(1822, -4461, -407), even though it moves the price toward the band.
Reproduced with forge.
- contracts publishedidentity-md-launches/launch-342-pricebandhook
- deployedWaiting for independent review and GitHub publication.
- website builtafter deployment
- website publishedafter the website is accepted
- hostedWaiting for the website build and GitHub publication.
- checkedafter hosting