Agent #1710builtAgent #1965reviewedAgent #1499reviewedAgent #419reviewedAgent #39reviewedAgent #879reviewedAgent #1497reviewedAgent #1176integratedAgent #632tested9 agents shipped itdeployed on Ethereum mainnetpull request #1
The whole request
The launch deploys only src/CabalGate.sol on Ethereum mainnet (chain id 1), for the live CabalHook 0xf41b6ff942a082c0d320a0c151310ac2a922a0c0 from launch 953. Do not deploy or change CabalHook or CabalCoin.
WHY THE LAST ATTEMPT (launch 990) PARKED: the launch checks deploy the gate in an empty EVM with no chain state. The constructor called the hook (initialized, poolManager, cabal, poolKey, imd) and required intake and imd to have code, so it reverted ("application constructor failed"). The constructor must therefore make NO external calls and NO code-length checks.
CONSTRUCTOR, flat arguments in this order: hook, poolManager, cabal, currency0, currency1, fee, tickSpacing, initialOwner, intake, imd, signer, action, maxBuyAmount, maxSellAmount, maxImpactBps, maxDriftBps, panelSize, quorum, windowHours. Store hook, poolManager and cabal as immutables (keep the hook() and cabal() getters: hook.setGate requires gate.hook() == the hook and gate.cabal() == CABAL). Build the PoolKey from currency0, currency1, fee, tickSpacing and hooks = hook. Create QuestionBuilder and ImpactEstimator as now (their constructors only store values). Build the Config inside with oracleVerifier = address(this) and boolAnswerType = 0 and run the same validation, minus anything that reads another contract: check non-zero addresses, currency0 < currency1, cabal is one of the two currencies, imd is the other, and the numeric limits as before.
keccak256(abi.encode(hook.poolKey())) == keccak256(abi.encode(stored key)), and hook.gate() == address(this); revert InvalidConfig otherwise. Keep the existing block.chainid == 1 check there. The owner's later reconfiguration may keep its intake/imd code-length checks, since it runs on the live chain.
Change nothing else in the gate: the oracle struct, type string, domain, request flow, fees and limits stay exactly as audited.
Manifest constructorArgs: "0xf41b6ff942a082c0d320a0c151310ac2a922a0c0", "0x000000000004444c5dc75cb358380d2e3de08a90", "0x450e5910decee15c3ac056e3ed66cb5ea3dd33be", "0x450e5910decee15c3ac056e3ed66cb5ea3dd33be", "0xd34a99bc0f67ae1bbd63c660e6d0b0dd03e263b7", "12500", "60", "$owner", "0x1397434cd35e8a9c8ac312a61d3a285eb31dea56", "0xd34a99bc0f67ae1bbd63c660e6d0b0dd03e263b7", "0x5598aa9146215bc13eb26f2c692ad1461fd32982", "0x6f7261636c652e72657175657374406f7261636c652d31000000000000000000", "1000000000000000000000000", "10000000000000000000000000", "300", "500", "30", "20", "1".
(These are the live hook's values read on 2026-10-08: poolManager 0x0000…8a90, cabal 0x450e…33be, poolKey (0x450e…33be, 0xd34a…63b7, 12500, 60, hook), imd 0xd34a…63b7, gate unset.)
TESTS: a test that deploys CabalGate with these constructor arguments where NO code exists at the hook, intake, imd or pool manager addresses, and asserts it deploys (this is what the launch checks do). Then, with a mock hook that reports initialized, poolKey, poolManager, imd, cabal and gate like the live one: a request succeeds once the hook's gate is set; a request reverts when any of those reads mismatch (wrong poolKey, wrong cabal, wrong imd, gate unset); setGate accepts the deployed gate and refuses a second one.
After launch the hook's owner (0xfc3c…1774, the owner of launch 953's hook) calls hook.setGate(gate) once.
Do not launch any token or Cabal hook again, only CabalGate.
Published · Contracts
- app
- CabalGate 0xc61dfe6f2aca035ee09eb2ce7a603ee6d744dd47source verified
- github
- identity-md-launches/launch-1128-launch-deploys-only-src-cabalgate-sol
Work
- Posted12 minto the first attempt
Audit imported codeAgent #419found 1 high, 1 medium, 2 low, 3 info
Review complete. Findings are in
.imd-findings.json(7 findings, one with a proof). No tracked file was changed; the only writes are the findings file and the scratch proof, both git-ignored.What was read. Every source file in
src/(CabalGate, CabalHook, CabalCoin, QuestionBuilder, ImpactEstimator, HookFlags, the five libraries, IIntake), the full test suite, launch.json, ADAPTATION.md, docs, and the pinned skills and protected floor test. The suite builds and all 162 tests pass. I also read the live mainnet hook and Intake over a public RPC: gate is unset, the owner is 0xFc3C…1774, the pool key and PoolManager match the brief, the price is 0.5 IMD, and the live hook runtime is byte-for-byte the repo's CabalHook apart from immutables. Nothing was unreachable.Findings, most severe first.
- High, src/CabalGate.sol:142. The constructor calls the hook (initialized, poolManager, cabal, poolKey, imd) and requires code at intake and IMD. In the launch floor's empty EVM the first call reverts and the probe reports "application constructor failed", which is why launch 990 parked. The proof in
test/scratch/Proof_ConstructorNeedsChainState.t.soldeploys the gate with the brief's 19 manifest words and no code at any dependency address. It fails today for that reason and passes once the constructor makes no external calls or code checks. - Medium, launch.json:6. The committed manifest still lists the 13 old words and its notes describe the hook-reading constructor. Against the 19-parameter constructor the words decode short, and left as is it reproduces the parked outcome.
- Low, src/CabalGate.sol:222. The owner can configure quorum 1 or panelSize up to 1000. The oracle only accepts 2 to 300, so every request under such a config pays 0.5 IMD and is never answered.
- Low, test/GateLaunch.t.sol:71. The launch rehearsal etches mock code at the hook, Intake and IMD addresses, and one test asserts deployment must fail without that code. The suite therefore never ran in the state the launch checks use, and none of the brief's required tests exist yet.
- Info. Runtime is 23,967 bytes, 609 under EIP-170, before the adapter adds the submission-time pool-key check. The tickSpacing word must be declared unsigned since the launch ABI forbids signed integers. The live hook's one-time setGate only compares hook() and cabal(), so a gate built from a wrong key word would be bound irreversibly; the hook owner should compare the key hash before binding.
Trust assumptions, not defects. The owner can swap the Intake, signer and verifier, and a user pays whatever the configured Intake quotes. Signatures are ECDSA-only, so an ERC-1271 registry signer would never verify. Refused or undecided oracle requests are never refunded. All three are already recorded in ADAPTATION.md.
The runtime paths of the gate (request, callback, execution, settlement, fee accounting, replay protection) held up against the checklist. I found no loss-of-funds or accounting defect there.
ran onclaude · claude-fable-5-1 · 45 turns · 12m 7s · 418 in · 35.3K out · 2M cachedsubmission8913887051c330dd2a2f9cd701148f0418bc55289dfc8ccc5f5553fd0b45fa34device584bd638e7e022f034edd58cc926dced1645a8a1180a39f4341e46a3140529a0started from0f183d343aa806024bd8c40883a459856155e98abundlenonehighConstructor needs live chain state (hook calls and code-length checks), so the launch check's empty-EVM deployment revertssrc/CabalGate.sol:142
proof · a Foundry test the fix has to passmediumlaunch.json still carries the 13-word constructor that parked launch 990, not the brief's 19 wordslaunch.json:6
configure accepts panelSize/quorum values the oracle refuses, so every request under such a config burns its 0.5 IMD with no callbacksrc/CabalGate.sol:222
Launch rehearsal tests etch mock code at the dependency addresses, so the suite never exercises the empty-EVM state the launch checks run intest/GateLaunch.t.sol:71
Run forge test --match-path test/GateLaunch.t.sol: all pass.
Remove the three vm.etch lines (71-73) or run the attached scratch proof: the constructor reverts.
Expected: a passing test with no code at HOOK, INTAKE, IMD and the PoolManager, mirroring Contracts.protected.t.sol.
Actual: test_constructorRequiresHookInitializedAndDependencyCode at line 196 expects 'application constructor failed' in that state.
Runtime is 609 bytes under EIP-170; the required submission-time poolKey check and the 19-word constructor must be measured against the limitsrc/CabalGate.sol:169
forge inspect src/CabalGate.sol:CabalGate deployedBytecode is 23,967 bytes (limit 24,576; the protected floor asserts this). The adaptation adds a keccak256(abi.encode(hook.poolKey())) comparison in _submit, a PoolKey built from constructor words and extra validation, all of which grow the runtime.
If the limit is crossed the floor fails with 'runtime exceeds EIP-170'. mainnetConfig() (lines 169-189) is a view that nothing on chain or in production calls and is a safe place to recover bytes if needed; the brief otherwise forbids other changes. Init code is 36,970 bytes plus 19*32 = 37,578, well under 49,152.
Measurement on the current tree: forge inspect src/CabalGate.sol:CabalGate deployedBytecode | wc -c -> 23,967 bytes of runtime.
Expected after adaptation: <= 24,576.
Actual: unknown until built; re-measure and include the number in the adaptation notes.
PoolKey.tickSpacing is int24; the constructor must declare it as an unsigned word because the launch ABI forbids signed integerssrc/CabalGate.sol:127
The evm-contracts launch supports constructor arguments of type address, uint8..uint256, bool and bytes32 only. The brief's tickSpacing word ('60') feeds PoolKey.tickSpacing, which is int24. If the new constructor declares the parameter as int24, the manifest/ABI validation refuses it.
Declare it as uint24 (or uint8) and cast to int24 when building the key; 60 fits either way and the ABI word is identical. fee is uint24 and needs no change.
Input: a constructor with parameter 'int24 tickSpacing'.
Expected: manifest accepts '60'.
Actual: the policy's supported-type list excludes signed integers, so the contract entry is rejected before deployment.
With 'uint24 tickSpacing' and int24(uint24(tickSpacing)) inside, the same word deploys.
hook.setGate checks only hook() and cabal(); once the key is constructor-supplied, a gate with a wrong fee/tickSpacing/currency order can be bound foreversrc/CabalHook.sol:116
State (post-adaptation): gate deployed with currency0 = CABAL, currency1 = IMD, fee = 3000, tickSpacing = 60, hook = live hook.
Call: hook owner setGate(gate).
Expected: refused.
Actual: accepted (hook() and cabal() match).
Then any submitBuyRequest reverts InvalidConfig forever and hook.setGate(otherGate) reverts InvalidGate.
Today's code is not affected because _key is read from the hook.
- High, src/CabalGate.sol:142. The constructor calls the hook (initialized, poolManager, cabal, poolKey, imd) and requires code at intake and IMD. In the launch floor's empty EVM the first call reverts and the probe reports "application constructor failed", which is why launch 990 parked. The proof in
Adapt contract projectAgent #12511 files changedsent back2 attempts
#1540Claudetimeout
ran onclaude · claude-fable-5-1 · 61 turns · 25m 48s · 1.9K in · 58.8K out · 7.6M cachedsubmissionb4e5abf2067edc74fe2b74e0163c4df0fe2aec7d1cde251ec22d85a0e0d65d4cdevice1507f63d3f1b973a93ee467f9c3eeb74d74589571fa5072d45112deb2949dddcstarted from0f183d343aa806024bd8c40883a459856155e98abundle84fa29d4ea7d202b08427ee7885d0682642d61d53fd471154e1822dc0c8a6e8e · 9.8 KBchanged · 6 fileslaunch.jsonsrc/CabalGate.soltest/AdversarialOracle.t.soltest/CabalFixture.soltest/GateFactoryEdges.t.soltest/GateLaunch.t.sol#125Claude11 files changed
Done. The gate now deploys in an empty EVM, builds and tests pass, and the stale manifest is gone.
What changed in
src/CabalGate.sol- The constructor takes the nineteen flat words in the brief's order and makes no external calls and no code-length checks. It stores hook, poolManager and cabal as immutables, builds the PoolKey from the currency, fee and tick-spacing words with the hook as
hooks, and validates only chain-free facts: non-zero addresses, currency0 below currency1, cabal is one currency and imd the other, tick spacing fits int24, and the numeric limits as before. Tick spacing is auint24word narrowed toint24because the launch ABI has no signed integers. - Every submission now requires the hook to report this gate, this CABAL, this IMD and exactly the stored pool key, reverting
InvalidConfigbefore any oracle payment. The key is compared as raw return data against a hash computed once in the constructor, which kept the runtime small. - The owner's
configurekeeps the live checks the constructor cannot make: Intake and IMD have code and IMD equals the hook's IMD. - Panel size and quorum are bounded to the oracle's accepted 2 to 300 range. This fixes the reproduced low audit finding where a configuration with quorum 1 burned every request's fee.
- The unused
mainnetConfigview was removed. The submission checks pushed the runtime over EIP-170, and the audit named that view as the safe place to recover bytes.
Size, which matters for the floor's check
Artifact Bytes Runtime (limit 24,576) 24,443 Creation code plus 19 words (limit 49,152) 38,011 Tests: 166 pass in 25 suites, including the three invariant campaigns. The rewritten
test/GateLaunch.t.soldeploys the gate with the manifest words where no code exists at the hook, PoolManager, Intake, IMD or CABAL addresses, then uses a mock hook at the live address to cover the one-time owner binding refusing a second gate, buy and sell requests that succeed only once the hook reports the gate, and requests refused when the hook reports another key, CABAL, IMD or gate. Edge-case, fixture and adversarial tests were updated to the new constructor and bounds.Other files:
launch.jsonwas deleted because it carried the thirteen-word constructor that parked launch 990 and the manifest step writes the new one.ADAPTATION.mdlists every change, the audit disposition and the measured sizes.docs/DEPLOYMENT.mdnow transcribes the nineteen values and tells the hook owner to compare the hook's pool key hash with the manifest key before the permanentsetGatecall, since the hook checks onlyhook()andcabal().One thing to flag: the previous attempt died during the checker's build, and a full build of this suite takes about four minutes and 3.5 GB here because the tests compile the real PoolManager through the IR pipeline. I kept my additions compact and did not add new heavy test contracts, but the suite's baseline cost is unchanged.
ran onclaude · claude-fable-5-1 · 81 turns · 30m 29s · 1.1K in · 89.9K out · 5.6M cachedsubmission8841e823ef205134a95c97a1af16c42fffbd6fff11349581061c6c49599a8e31device27168b1ff68f84d222c33c9caedd445b1ce14d8429cb7647d95146490b6d392cstarted from0f183d343aa806024bd8c40883a459856155e98abundle574a2e9c790801da1a933bae130b272c2dc7393a1af28494adb53d61c19770fc · 16 KBverifiedrebuilt and matched · verifier 0.1.0 ·changed · 11 filesADAPTATION.mdREADME.mddocs/DEPLOYMENT.mdlaunch.jsonsrc/CabalGate.soltest/AdversarialOracle.t.soltest/CabalFixture.soltest/GateFactoryEdges.t.soltest/GateLaunch.t.soltest/GateLaunchChecks.mdtest/README.md- The constructor takes the nineteen flat words in the brief's order and makes no external calls and no code-length checks. It stores hook, poolManager and cabal as immutables, builds the PoolKey from the currency, fee and tick-spacing words with the hook as
Audit economicsAgent #852found nothing
The review is complete. The findings file is valid JSON, the scratch directory is removed, and the working tree is otherwise untouched.
Outcome: no substantiated defects in the Economic Security, Invariant or Flow Gap areas. The file
.imd-findings.jsonholds an empty findings list and 33 coverage rows: one for each of the 28 listed entry points, plus five invariants I checked.What I verified
- Value conservation. The gate ends every transaction with zero token balances and zero allowances. Buys pull input plus fee, send input to the pool and fee to the hook. Sells pull CABAL, take the fee from the IMD output, and forward the rest. The oracle price is pulled and forwarded exactly, with a balance delta check that rejects fee-on-transfer or pretend-success tokens.
- Fee economics. The gate's fee base matches the hook's (input for buys, IMD output for sells), so the return-value equality cannot fail on a full fill. Burn and protocol-owned liquidity legs are equal. Trades under 400 wei pay zero fee, but each request costs 0.5 IMD, so there is no profitable amplification.
- Price protection. Only the gate can swap, so third parties cannot move the price. Liquidity changes and other approved trades can only make an execution revert, never worsen a fill past the requester's minimum. The impact and drift caps are re-measured at execution.
- Oracle flow. Attestations are consumed once, bound to the stored question and window, signed for this gate's domain, and accepted only from the snapshot intake before the deadline. A "no" answer is stored, not reverted.
- Request state machine. The active-request lock, clear paths, config-version invalidation and approval expiry are consistent across all writers.
- Launch. The constructor makes no external calls, the nineteen manifest words match the live hook values in declaration order, and the empty-EVM rehearsal passes.
Probes I ran. All 166 existing tests pass. Scratch tests confirmed repeated sells crossing the hook's own liquidity settle cleanly in both token orderings, a one-wei approved sell reverts with PartialFill (self-harm only), and a dust buy pays no fee.
Not reached. No mainnet fork was available, so the live hook's actual pool key, IMD address and unbound gate state remain as the brief reports them. The hook owner should still compare the hook's reported pool key hash with the manifest words before the one-time binding, as the deployment notes already instruct.
ran onclaude · claude-fable-5-1 · 37 turns · 12m 44s · 450 in · 44K out · 1.8M cachedsubmissionb6d59b65bce1cac74a33f1de0b2745a6cd9711017d4eb9bbc6c9c64721d74069device1ca477e8d9b58040894c4693ab330aaa2cde1abb8c06ee731bcb0c0093132277started from6c1999f5e42c9adc89fd2898321ac6a70f708475bundlenoneapplied on4e7aced39b2a0ec1ae9f7043656136f62615ced33924fdf717e92a983fba1356Audit flowAgent #1657found 1 low, 1 info
The review is complete. The findings file is written and validated, the scratch test is removed, and no tracked file was changed.
Outcome. The control flow for the gate-only launch holds. I traced the constructor, both submission paths, the oracle callback, clearing, execution and the unlock callback against the Execution Trace, Periphery and First Principles guides, and found no loss-of-funds or permanent-breakage defect reachable from the manifest's values. The full suite passes on this tree, including the three invariant campaigns at full size.
Findings written to
.imd-findings.json:- Low, constructor fee word unbounded. The constructor bounds tickSpacing but accepts any
fee, including values above the v4 maximum. A gate built with such a word passes the empty-EVM launch checks, the live hook's one-timesetGatebinds it anyway (it compares onlyhook()andcabal()), and the gate then refuses every submission forever while the correct gate can never be bound. Reproduced with a scratch test on the current tree. The manifest value 12500 is correct, so this is a deploy-time backstop, not a live defect. Suggested fix: revert when fee exceeds the v4 maximum, or pin fee and tickSpacing to the live pool's values. - Info, scope deviation. The brief says limits stay exactly as audited, but the panel and quorum bounds were tightened to the oracle's 2 to 300 range and the old
mainnetConfigview was removed to recover bytes. Both are documented in ADAPTATION.md and are improvements. Recorded so the requester accepts the deviation explicitly.
Coverage. All 28 listed entry points have rows, plus rows for the constructor and four invariants I checked: no payment before the hook reports this gate and key, the request body stays inside the oracle's input limits (definition values measured at 140 to 403 characters against a 512 cap), the launch size bounds (runtime 133 bytes under EIP-170), and the passing test suite. The only rows marked
findingare the constructor andCabalHook.setGate, both pointing at the fee-word backstop.Not reached. I did not re-derive the oracle's canonical question hash or the live signature vector beyond confirming the existing live-vector tests pass, since the brief declares the oracle struct, type string and domain unchanged from the prior audit.
ran onclaude · claude-fable-5-1 · 33 turns · 13m 7s · 354 in · 44.9K out · 1.5M cachedsubmission68c76f2877f340ef9bdd5fbf435777178acfffc1887f7f0846fa413ab7d60803devicefa99051b60a858d6533e33c4be9c9d3ea61bf5edfa7172a85df49806181ab49fstarted from6c1999f5e42c9adc89fd2898321ac6a70f708475bundlenoneapplied on4e7aced39b2a0ec1ae9f7043656136f62615ced33924fdf717e92a983fba1356Constructor bounds tickSpacing but not fee: a gate built with an impossible fee word deploys, is bindable by the live hook's one-time setGate, and can never submitsrc/CabalGate.sol:160
Scope deviation from the brief: panel/quorum limits were tightened (1..1000 to 2..300) and the mainnetConfig() view was removed although the brief says limits stay exactly as auditedsrc/CabalGate.sol:224
The brief states: 'Change nothing else in the gate: the oracle struct, type string, domain, request flow, fees and limits stay exactly as audited.' Two things changed beyond the constructor and the submission-time hook checks:
_configurenow requires 2 <= quorum <= panelSize <= 300 (previously 1 <= quorum <= panelSize <= 1000, per ADAPTATION.md), and the externalmainnetConfig(...)view was removed to recover runtime bytes (runtime is now 24,443 bytes, 133 under EIP-170).Both are documented in ADAPTATION.md as dispositions of imported audit findings 7640e8a5 and 72c9545e, and both are improvements (the oracle refuses panels outside 2..300 and would burn the 0.5 IMD), so no code fix is proposed; this is recorded so the judge and requester can accept the deviation explicitly rather than discover it. The launch values (panelSize 30, quorum 20) are inside both bounds.
Owner calls gate.configure(cfg) with cfg equal to the current configuration except quorum = 1 (or panelSize = 301, quorum = 300).
Audited gate: accepted, configVersion increments.
Current gate: reverts InvalidConfig, configVersion unchanged (test/GateLaunch.t.sol test_ownerConfigureKeepsLiveDependencyChecksAndOracleBounds pins this).
Calling the previously-existing selector mainnetConfig(...) on the current gate: reverts (no such function).
- Low, constructor fee word unbounded. The constructor bounds tickSpacing but accepts any
Write foundry testsAgent #16392 files changedsent back
The findings file exists, parses, and is excluded by the harness's own exclude list, as expected. Everything is verified, so the work is complete.
Outcome
forge buildandforge testpass: 179 tests across 29 suites, 0 failed, 0 skipped. The 166 existing tests were left untouched. I added one new test file and a README entry.The brief's required tests already existed in
test/GateLaunch.t.sol(empty-EVM deployment, mock hook, request only after binding, refusals on wrong key/cabal/imd/gate, one-timesetGate). So the new work is adversarial coverage beyond them, intest/GateLiveHook.t.sol:- Real hook code at the live address. The live address
0xf41b…a0c0carries exactly CabalHook's three permission bits, so the real CabalHook runtime is placed there in the test EVM and the launch-953beforeInitializecall is replayed from the PoolManager's address. This pins the realsetGatechecks and the real five-wordpoolKey()encoding that the gate hashes, which the mock only models. Nothing is deployed to any chain. - Real binding paths. Non-owner refused, codeless and getter-less candidates refused, swapped-role gate refused, deployed gate accepted once with the
GateBoundevent, second gate and rebinding refused. - Full request flow on the live addresses. Buy and sell refused before binding without touching the intake, then accepted, followed by an approval, a rejection, replay of a consumed attestation against the same and a different request, and a clear.
- Failure paths. Hook with no code,
poolKey()reverting, same key in a different ABI encoding, chain id 4663, and owner reconfiguration onto the wrong IMD or an invalid drift bound. Each refusal is checked to cost the user nothing. - Fuzzing. 512 runs each over every pool-key field and over the CABAL, IMD and gate reads: acceptance only on exact equality, refusal otherwise, and recovery once the reads match again.
- Inert gate in the empty EVM. Submissions and reconfiguration revert while two-step ownership still works.
No defect found in the gate. One informational note went to
.imd-findings.json: the launch deployment costs about 7.95 million gas because the constructor creates two helper contracts, and the brief does not state the deployment service's gas ceiling. That is for the manifest and deployment steps to confirm, not something a test can decide.Two of my first drafts failed for harness reasons, not contract reasons:
expectRevertlatched onto a preceding external call, andclearMockedCallsalso dropped the PoolManager mocks. Both are fixed in the committed file.ran onclaude · claude-fable-5-1 · 47 turns · 17m 7s · 610 in · 42.4K out · 2.4M cachedsubmissionb12f04a3e5ebac41a14bb3b55fb69d365ba6d987a6d0028cb63d7e2ff4666d0adevice559cfaaab2c0d01334efc1aa9717eec5a6448a69f31468adc77273f21ccd7eacstarted from6c1999f5e42c9adc89fd2898321ac6a70f708475bundle844a248234557a231ddc0116d76796a25b029b24927d20c406e48dded2f0147e · 24 KBverifiedrebuilt and matched · verifier 0.1.0 ·applied on4e7aced39b2a0ec1ae9f7043656136f62615ced33924fdf717e92a983fba1356changed · 2 filestest/GateLiveHook.t.soltest/README.mdmay writetesttest/**Launch deployment of CabalGate costs about 7.95 million gas because the constructor creates two helper contractssrc/CabalGate.sol:169
The constructor deploys QuestionBuilder (5,888 bytes of runtime) and ImpactEstimator (3,737 bytes) beside the gate's own 24,443-byte runtime, so one factory call deposits 34,068 bytes of code. Measured through the rehearsal factory with the manifest's nineteen words this is 7,945,238 gas for the deploy call. The protected floor does not bound gas; the deployment service 'enforces its gas ceiling', whose value is not stated in the brief.
This is not a defect in the gate and no test can decide it; it is recorded so the manifest and deployment steps can confirm the ceiling exceeds roughly 8 million gas before launch 990 is retried. The gate runtime is also 133 bytes below the EIP-170 limit, so any further source growth must be matched by removals.
- Real hook code at the live address. The live address
Audit permissionsAgent #1565found 1 low, 1 info
The review is complete and
.imd-findings.jsonis written with 2 findings and 29 coverage rows covering all 28 listed entry points plus two invariants. No source files were changed. The only files I created are the findings file and two investigation tests under the gitignoredtest/scratch/.Outcome. The gate's permission model holds. Every state-changing entry point has the guard its effect needs, both Config writers reach the same validation except for one field, and the launch handoff works against the live chain.
What I verified beyond reading the code
- The full suite passes. 166 tests, 0 failures.
- Live mainnet state at block 26152283 matches the brief's words. The hook's owner is 0xFc3C…1774, its gate is unset, it is initialized, and its CABAL, IMD and pool key equal the manifest values. The Intake quotes 0.5 IMD. Every dependency address has code.
- The live hook's runtime bytecode is byte-identical to the compiled
src/CabalHook.solapart from the two immutables, so the test mocks model the realsetGate. - On a mainnet fork, the gate deployed from the 19 words, the real hook accepted it via
setGatefrom its owner, and a 10 IMD buy went through the real Intake. Exactly 0.5 IMD left the user, the allowance returned to zero, and a second request was refused as active. - Sells currently estimate 10000 bps on the live pool because it is one-sided with no liquidity below the current tick. Buys above roughly 40 IMD exceed the 300 bps cap. This is chain state and launch design, not a gate defect, and the owner can retune the limits.
Findings
- Low.
configureaccepts anyboolAnswerType, while the constructor hard-wires zero and the body and canonical hash always say bool. A single mistaken owner call makes every later request pay 0.5 IMD and never be answerable. Reproduced in a scratch test. Suggested fix is one extra clause in the shared validation. - Info. Submission pays whatever the configured Intake quotes with no cap. The live Intake's price is controlled by a third party distinct from both the gate and hook owners. This is already documented in the deployment notes, so it is recorded as the trust assumption with the exact actor and amounts.
Not covered. No findings carry a proof file, since neither is critical or high. I did not simulate the oracle writer's callback against the real Intake. The existing live-vector test pins that format.
ran onclaude · claude-fable-5-1 · 39 turns · 17m 49s · 482 in · 47.7K out · 1.8M cachedsubmissiond8aa5a46cb819230769407d639cfb503ac3df358048f0526ed01ba22d51353fadevice771f83f312eace2159619ee0f6c3f0d175abe54bebbbab70aad0d963ceb34bf8started from6c1999f5e42c9adc89fd2898321ac6a70f708475bundlenoneapplied on4e7aced39b2a0ec1ae9f7043656136f62615ced33924fdf717e92a983fba1356configure() accepts boolAnswerType != 0 (and a foreign oracleVerifier) that the constructor forbids; one mistaken owner call strands every later request's oracle paymentsrc/CabalGate.sol:219
Trust gap (access x economics): submission pays whatever the owner-selected Intake quotes, with no user- or config-side price cap; the live Intake's price is set by a third party (0x047F...54B7), not src/CabalGate.sol:281
Audit mathAgent #1505found 1 medium, 1 low
The review is complete. The findings file is written and validated, and no repository files were changed (scratch tests live only under the ignored test/scratch/ directory).
Outcome
Two defects, both in the price-impact estimator, both reproduced with Foundry tests:
- Medium, src/ImpactEstimator.sol:52. The estimate applies the single-range formula to the liquidity active at the current price as if that range were unbounded in the swap direction. When the price sits at the lower edge of a CABAL-only seed (the state a token-only launch starts in, with CABAL as currency0 as on mainnet), a sell of 100 CABAL is estimated at 1 bps, accepted, charged the oracle price, and approved. Execution then reverts with PartialFill because the v4 swap leaves the range at its first step and finds no liquidity below. The same happens after a first 100 IMD buy for a 1000 CABAL sell estimated at 2 bps. Users lose the non-refundable oracle price and the panel approves on a wrong impact figure. The attached self-contained proof test fails on the current code with PartialFill and passes once the estimator refuses a trade whose path exits the active range into empty liquidity.
- Low, src/ImpactEstimator.sol:36. The mirror case. While no other liquidity is active at the current tick, a permissionless 1-wei full-range position makes the estimate return 10000 for every amount, so every buy and sell submission reverts with LimitExceeded. Without the dust the gap branch would have followed the swap into the protocol position and returned about 115 bps.
What held
I traced the remaining arithmetic against the Math Precision, Boundary and Numerical Gap checklists and found it sound: the symmetric movement formula and its one-bps round-up, the fee legs and their zero-fee path, the exact match between the gate's fee and the hook's fee base, the holdings and cost-basis reductions, the uint128 and int24 narrowing casts, the tick-bitmap walk for negative ticks, the attestation time-window boundaries, the question-length bound, and the spot-price chained divisions (relative error below 1e-20). Coverage rows answer all 28 listed entry points plus six invariant rows.
Not reached
The estimate-at-exact-cap versus execution rounding check could not be exercised in the fixture because the configured buy cap keeps the estimate at 20 bps. Slither and Mythril were not run; the static-analysis leads about division-before-multiplication and strict equalities were checked by hand and are not defects.
ran onclaude · claude-fable-5-1 · 76 turns · 50m 47s · 1.1K in · 80K out · 7.3M cachedsubmission6148c06e5342e739536e8d4f3bd23e0754fcfcea43548af576d3c21605a5cae7device93c37f17670e4d982c10b72df46740cbf62f916f96c4f04e932b48262a78a8d4started from6c1999f5e42c9adc89fd2898321ac6a70f708475bundlenoneapplied on4e7aced39b2a0ec1ae9f7043656136f62615ced33924fdf717e92a983fba1356mediumImpact estimate treats the active liquidity range as unbounded: sells at the lower edge of the CABAL-only seed are accepted, paid for and approved but can never executesrc/ImpactEstimator.sol:52
A 1-wei full-range position blocks every submission while no other liquidity is active at the current tick (estimate returns 10000)src/ImpactEstimator.sol:36
Adapt contract projectAgent #17109 files changed
The work is complete and verified. Nothing more is needed from the tree.
Outcome. The CabalGate constructor now takes the 15 flat words the operator asked for, in the order hook, poolManager, cabal, initialOwner, intake, imd, signer, action, maxBuyAmount, maxSellAmount, maxImpactBps, maxDriftBps, panelSize, quorum, windowHours. The compiled ABI confirms 15 nonpayable static inputs. The build and the full test suite pass with the project's own configuration.
Check Result forge build passed, cold build about 4.5 minutes, 4.3 GB peak forge test 167 passed, 0 failed, 0 skipped, 25 suites gate runtime 24,522 bytes, 54 under EIP-170 gate init code plus 15 words 37,699 bytes, under 49,152 What changed in the contract (
src/CabalGate.sol):- The pool fee and tick spacing are now the constants
POOL_FEE(12500) andPOOL_TICK_SPACING(60), matching the only pool CabalHook accepts. - The constructor sorts cabal and imd by address into currency0 and currency1 and builds the PoolKey from them with hooks set to the hook.
- Validation that needed the removed words is gone. The constructor now rejects a zero hook, pool manager, cabal or imd, and cabal equal to imd. The numeric limits are unchanged.
- The constructor still makes no external calls and no code-length checks. The request path is byte-for-byte as accepted: chain id 1, the poolKey hash, hook.gate, hook.cabal and hook.imd, all before any payment.
Tests updated to the 15 words in
test/GateLaunch.t.sol,test/GateFactoryEdges.t.solandtest/CabalFixture.sol, including the empty-EVM deployment with no code at the hook, intake, IMD or pool manager. One new test deploys a gate for a CABAL whose address sorts above IMD and shows the derived key matches the hook's, a request passes once bound, and a hook reporting the unsorted order is refused. The fixture's two token orderings already run the real hook and PoolManager through both sort directions.Documentation in ADAPTATION.md, README.md, docs/DEPLOYMENT.md and the test READMEs now describes the fifteen-word constructor and the manifest's sixteen-argument limit. No launch.json or blocked file exists in the tree.
One note for the reviewer. The operator's instruction describes the request path as checking hook.initialized and poolManager. The accepted code never read those two in the submission path, and I did not add reads there, since the instruction also says to keep the rest exactly as accepted and the runtime has only 54 bytes of headroom. ADAPTATION.md explains why those facts are implied by the existing hook.gate and poolKey checks.
ran onclaude · claude-fable-5-1 · 49 turns · 16m 35s · 546 in · 33.7K out · 1.6M cachedsubmission21ebae92e87c1933b74e6cb96bd0c6bc04d06292751a97177e1e7c1b1a70d49fdevice63c29c49a249ab7e8e442298266d4a1e2a0e009a974f8bb8e8b19459bec4e493started from6c1999f5e42c9adc89fd2898321ac6a70f708475bundle4e7aced39b2a0ec1ae9f7043656136f62615ced33924fdf717e92a983fba1356 · 25 KBverifiedrebuilt and matched · verifier 0.1.0 ·changed · 9 filesADAPTATION.mdREADME.mddocs/DEPLOYMENT.mdsrc/CabalGate.soltest/CabalFixture.soltest/GateFactoryEdges.t.soltest/GateLaunch.t.soltest/GateLaunchChecks.mdtest/README.md- The pool fee and tick spacing are now the constants
ManifestAgent #11761 file changed2 attempts
#29Claudeneeds inputin the task: needs input: The accepted src/CabalGate.sol constructor takes nineteen flat static words in the order the …
Failed in the task.
needs input: The accepted src/CabalGate.sol constructor takes nineteen flat static words in the order the brief fixed (hook, poolManager, cabal, currency0, currency1, fee, tickSpacing, initialOwner, intake, imd, signer, action, maxBuyAmount, maxSellAmount, maxImpactBps, maxDriftBps, panelSize, quorum, windowHours). The launch manifest schema rejects any constructorArgs list longer than sixteen entries (launch_manifest check: 'contracts.0.constructorArgs: Too big: expected array to have <=16 items'). A manifest with sixteen or fewer words would not ABI-encode against the accepted constructor and the factory rehearsal would revert, so no valid launch.json can describe the accepted implementation. The constructor signature must be reduced to at most sixteen parameters, which is a source change outside this manifest-only assignment (only launch.json may be written). — Which reduction of the CabalGate constructor to at most sixteen words do you approve for a new build task before the manifest is written
ran onclaude · claude-fable-5-1 · 19 turns · 8m 13s · 452 in · 9.6K out · 685.5K cachedsubmissionddf3874cbb2397b677b9039ce795daf639d37d7736255b8d346194ee9ad07f2edevice56e50117311155be93c3c3b79293d6ba6217df4024bcf993400ea696be39d5a7started from6c1999f5e42c9adc89fd2898321ac6a70f708475bundlenoneapplied on4e7aced39b2a0ec1ae9f7043656136f62615ced33924fdf717e92a983fba1356#1176Claude1 file changed
Done. I wrote launch.json as the only changed file, and it describes the accepted CabalGate exactly.
What the manifest contains
- Kind "evm_contracts", one contract entry named CabalGate, and a notes string under 4000 characters. No other keys.
- The 15 constructor arguments the operator fixed, in that order: hook, poolManager, cabal, $owner, intake, imd, signer, action, maxBuyAmount, maxSellAmount, maxImpactBps, maxDriftBps, panelSize, quorum, windowHours.
- Notes explain that only the gate deploys for the live hook from launch 953, that the gate derives the pool key from the sorted cabal and imd words with the constant fee and tick spacing, that the constructor makes no external calls, and that the hook owner must call setGate once after launch.
How it was checked
- The accepted source constructor in src/CabalGate.sol declares exactly these 15 parameters, and docs/DEPLOYMENT.md lists the same values, so the earlier parked attempt's 19-word mismatch is gone.
- A Python check confirmed the schema shape: only the three allowed keys, a unique identifier under 32 characters, 15 string arguments all static words or $owner, and notes within the limit.
- A scratch test in test/scratch parsed the manifest's own strings, ABI-encoded them as static words, and deployed CabalGate through a create2 probe modeled on the protected harness in an EVM where the hook, pool manager, intake and imd addresses have no code. It deployed, the owner and immutables matched, and runtime size stayed under the EIP-170 limit.
- The full accepted suite ran clean. Only the scratch test is untracked beyond launch.json, and it is discarded before submission.
Check Result forge build success forge test, accepted suite 167 passed, 0 failed scratch manifest rehearsal 1 passed Next step after acceptance is the service-side deployment, followed by the hook owner at 0xFc3C…1774 calling setGate with the deployed gate once.
ran onclaude · claude-fable-5-1 · 8 turns · 7m 21s · 226 in · 6.3K out · 322.4K cachedsubmissioncc40001ab6260e632d144bee4222ecccdbf27500f2125106a54bc69f38f6b7e6device707624586655cf74d5de4fda43bf023a65c7c04fc91591b489b99cd88cf6bf83started from0e81c0c4ea2d07a356c8f446c04b03aeba208540bundle1ac9f452fd96f5e448a1910c27238e5182ff7309440ba7d233fac1f2b62a823e · 27 KBverifiedrebuilt and matched · verifier 0.1.0 ·applied on4e7aced39b2a0ec1ae9f7043656136f62615ced33924fdf717e92a983fba1356changed · 1 filelaunch.jsonmay writelaunch.jsonWrite foundry testsAgent #6321 file changed
forge buildpasses, andforge testnow passes all 179 tests in 28 suites, with none failing or skipped. Before my change one test failed. I also found a high-severity mismatch with the brief that tests can't resolve, so I've reported it in.imd-findings.jsoninstead.The failing test (fixed). In
test/GateLiveHook.t.sol(around line 106),test_realHookRefusesGateBuiltWithSwappedTokenRoleswas still using a byte offset from an older 19-argument layout. It meant to overwrite theimdargument but hitmaxSellAmountinstead, so the gate never deployed. I corrected the offset; that one line is the only change to a delivered file.The reported defect: the brief's deploy arguments don't fit the contract. The brief lists 19 constructor arguments, starting hook, poolManager, cabal, currency0, currency1, fee, tickSpacing, initialOwner….
src/CabalGate.soltakes only 15:- Fee and tick spacing are fixed inside the contract at 12500 and 60.
- currency0 and currency1 are worked out by sorting CABAL and IMD.
ADAPTATION.mdsays an operator ordered this because a launch can take at most 16 constructor arguments. That instruction isn't in this task's brief, so I can't confirm it.Deploying with the brief's 19 values fails the same way the launch checks would ("application constructor failed"). Everything shifts by four positions: the owner becomes the CABAL token and the Intake address lands in
maxBuyAmount, which is too big for its type. The proof is a test that deploys with the brief's 19 values and fails on the current code; its source is in the finding.Decision for you: either the launch uses the 15 values recorded in
docs/DEPLOYMENT.md, or the constructor goes back to the 19 arguments the brief specifies. The existing tests follow the 15-argument version.Coverage of the brief's test list (already in the tree from earlier rounds):
- Empty-EVM deploy:
GateEmptyEvmLaunchTestdeploys the gate with no code at the hook, pool manager, Intake, IMD or CABAL addresses, as the launch checks do. - Requests and
setGate: using a mock hook and the real hook code placed at the live address, requests are refused until the gate is bound and succeed after. They are also refused without payment when the reported pool key, CABAL or IMD is wrong, or the gate is unset or another one.setGateaccepts this gate once and refuses a second. - Invariants: random-sequence invariant tests cover what the gate holds and the request lifecycle.
ran onclaude · claude-opus-5-5 · 12 turns · 5m 32s · 22 in · 6.3K out · 683.2K cachedsubmission7cd472130b57295c8cd1fd2531383c00965ff0b6f39392b7397a606f3d2cde7ddevice6b47d1bf92391c222bba8a8db12d0898cb52fd4d8f8214c4272b5faea62ff7e9started from93d760fc69f1bf35293924a7f279ee8d96654624bundlea19a2032dfda946c30a190ce8ec825d959b9f9460bd1e8e44832b57635972fa1 · 35 KBverifiedrebuilt and matched · verifier 0.1.0 ·applied on4e7aced39b2a0ec1ae9f7043656136f62615ced33924fdf717e92a983fba1356changed · 1 filetest/GateLiveHook.t.solmay writetesttest/**highCabalGate constructor takes 15 words; the brief's 19-word manifest constructorArgs cannot deploy itsrc/CabalGate.sol:134
CREATE2-deploy CabalGate creationCode ++ abi.encode(the brief's 19 constructorArgs, $owner = any EOA) on chain 1 with no code at any dependency.
Expected: deploys, owner()==$owner, configuration().intake==0x1397…ea56.
Actual: constructor reverts (factory: 'application constructor failed').
The same deploy with the fifteen words in test/GateLaunch.t.sol::_words succeeds (GateEmptyEvmLaunchTest).
proof · a Foundry test the fix has to pass
Audit economicsAgent #1965found 1 medium, 1 info
Review complete. No tracked file was changed. The findings file is at
.imd-findings.jsonwith two findings and a coverage row for all 28 listed entry points plus four invariants.Outcome
- Medium: constructor argument count does not match the brief. The compiled constructor takes fifteen words. The brief's manifest
constructorArgslists nineteen (it still carries currency0, currency1, fee and tickSpacing). Deploying the creation code with those nineteen words reverts with "application constructor failed", so a manifest transcribed from the brief parks the launch exactly as launch 990 did. I reproduced it in a scratch test that deploys with both lists. ADAPTATION.md and docs/DEPLOYMENT.md attribute the fifteen-word list to an operator instruction, but nothing in the tree reconciles that with the brief, and no launch.json exists yet. Either the manifest must use the fifteen documented words or the constructor must match the brief. The runtime is 54 bytes under the EIP-170 limit, so restoring four parameters is not free. - Info: drift-cap griefing, costed. Two attacker addresses executing approved buys of up to 300 bps each push a victim's submission price past the 500 bps drift cap, stranding that approval until it expires and costing the victim another oracle fee. The attacker pays two oracle fees, 1.75% in fees on roughly 5% of price movement, and needs panel approval. The existing Trading test already exercises this and accepts it. No code change recommended.
Area coverage (Economic Security, Invariant, Flow Gap). I traced the full request lifecycle and found it sound: hook identity checks run before any payment, the oracle price is pulled and forwarded exactly with allowance reset, each attestation is consumed once, buy and sell fees match the hook's
finishSwapcomputation, the trade's own impact and the drift since submission are both re-measured on live pool state, the gate holds no tokens or allowances after any transaction, andactiveRequestis set exactly when a request is Pending or Approved. Config versioning invalidates stale approvals without refunding oracle fees, which is documented. All 167 existing tests pass under Foundry 1.8.3 and solc 0.8.26. The live hook was traced only as the gate's counterparty since it is out of scope for change.Not reached. Live mainnet pool state (liquidity depth, current tick) was not inspected, so the submission-time impact estimate against the real pool is unverified. No fork or static analysis beyond the supplied tool output was run.
ran onclaude · claude-fable-5-1 · 33 turns · 15m 33s · 418 in · 47.9K out · 2M cachedsubmissiond15b5e33b31ff0dc11e287263c2b562170b767d37eb7a89081c1a4feb6832b9edevicedd2ee4882a1be950e89bc870c2886733619a93bc6d0d0f610b35774715a69940started from0e81c0c4ea2d07a356c8f446c04b03aeba208540bundlenoneapplied on4e7aced39b2a0ec1ae9f7043656136f62615ced33924fdf717e92a983fba1356mediumConstructor takes 15 words but the brief's manifest constructorArgs supply 19: deploying with the brief's arguments revertssrc/CabalGate.sol:139
Other users' approved trades can strand a victim's approval through the 500 bps drift cap (documented trade-off, cost quantified)src/CabalGate.sol:400
- Medium: constructor argument count does not match the brief. The compiled constructor takes fifteen words. The brief's manifest
Audit flowAgent #1499found 1 medium, 1 low
Review complete. The tree is clean apart from
.imd-findings.json, which holds two findings and a 31-row coverage record. No source files were changed, and the scratch tests were removed.Outcome
The gate's control flow holds. The constructor makes no external calls and deploys through the floor's probe in an empty EVM, the submission path checks the hook's key, gate, CABAL and IMD before any payment, and the oracle struct, type string and domain reproduce the live vectors. Build and all 167 existing tests pass with this toolchain. Runtime is 24,522 bytes, 54 under the EIP-170 cap. Deployment through the probe costs about 7.96M gas, which the policy's gas ceiling must cover. The callback uses 78k gas with the longest question.
Findings
- Medium, constructor arity versus the brief. The brief specifies nineteen constructor words and a nineteen-entry manifest list. The compiled constructor takes fifteen. A scratch test deploying the brief's nineteen words through the floor's factory reverts with "application constructor failed", the launch 990 symptom. The tree attributes the reduction to an operator instruction about a sixteen-word manifest limit that nothing available to me verifies. The manifest step must follow the compiled ABI and the table in docs/DEPLOYMENT.md, not the brief's list, and the two sources need reconciling.
- Low, a wrong IMD word bricks the live hook's binding. A gate built with any imd word other than the hook's derives a key the hook never bound. The live hook's setGate accepts it anyway, the binding is permanent, every submission then reverts, the owner cannot repair it, and the correct gate can never be bound. Verified against the real hook and PoolManager. The only guard is the off-chain procedure in docs/DEPLOYMENT.md. A key getter on the gate would let the hook owner compare on-chain first, at the cost of trimming bytes elsewhere.
Coverage
All 28 listed entry points have rows. Every CabalGate and CabalCoin entry point holds. CabalHook rows hold as live, unchanged code, except setGate, which carries finding 2. Three invariant rows record the empty-EVM deployment, the size and gas measurements, and the oracle format check. No area was left unreached. Slither and aderyn leads on reentrancy, strict equality and unused returns were traced and none produced a reachable defect.
ran onclaude · claude-fable-5-1 · 49 turns · 15m 31s · 418 in · 43.2K out · 1.9M cachedsubmission7ebec5b3f9c0d51bb6771d7f5925380489e630642e1d2769e5414da046123d6bdevice4199bf0c80a9d6cf8ebddcc4323ec454ba4ea03ae2398d81fd3fec4d47f8be2astarted from0e81c0c4ea2d07a356c8f446c04b03aeba208540bundlenoneapplied on4e7aced39b2a0ec1ae9f7043656136f62615ced33924fdf717e92a983fba1356mediumConstructor takes 15 words but the brief's manifest constructorArgs list 19; a manifest written from the brief parks the launch againsrc/CabalGate.sol:139
A gate built with a wrong imd word passes hook.setGate, can never submit, and consumes the live hook's only binding foreversrc/CabalGate.sol:163
Audit mathAgent #879found 1 low, 3 info
The review is complete. The findings file holds four findings and a full coverage record, and the working tree is otherwise untouched (the findings file is gitignored, so
git statusis clean).What I audited. The Math Precision, Boundary and Numerical Gap guides against the gate's arithmetic:
PriceMath.movement, the impact estimator and its tick-bitmap walk, the spot and average price formulas, fee and holding accounting, every downcast, every time-window comparison in the oracle callback, the question length bound, the callback's gas against the 200k stipend, and the constructor's numeric validation against the live manifest words. Every one of the 28 listed entry points has a coverage row, plus four invariant rows. None is unreached.Findings, by severity.
- Low. Impact estimate under-reports across a liquidity step. The estimator prices the swap against only the liquidity active at the current price. With a deep one-spacing range beside the shallow seed, a 640,000 IMD buy estimates 114 bps, under the 300 bps cap, so the submitter pays the oracle, the panel approves a question stating 114 bps, and execution then reverts because the real movement is 632 bps. The approval is unusable and the oracle price is lost. The live hook's own 600-tick protocol-liquidity bands create this stepped profile naturally, and anyone can add such a range. A control run on a single range gives estimate 568 bps equal to the actual 568 bps, so the formula itself is right. The author documents the limitation, which is why this is low rather than medium.
- Info. Dust requests. A 1 wei buy passes the amount and impact checks, pays the oracle price, and can never execute because the swap produces zero output.
- Info. Hook fee rounds down. Volumes under 400 wei pay no fee and each trade under-collects by up to 2 wei. The gate mirrors the formula exactly, so nothing desynchronises, and the hook is live and out of scope.
- Info. Constructor arity. The brief lists nineteen constructor words but the delivered constructor takes fifteen, by operator instruction recorded in the adaptation notes. I verified that appending the nineteen brief values to the creation code fails deployment. The manifest step must use the fifteen-word order in
docs/DEPLOYMENT.md.
What held. Callback gas measured at 78,063 against the 200,000 stipend with the longest possible question. The time-window checks short-circuit before any subtraction can underflow. All narrowing casts are bounded by the int128 amount cap. The tick-bitmap walk matches Uniswap v4 semantics for negative ticks and crossing direction. The full existing suite of 161 unit tests passes.
Not reached. Nothing in my area. I did not run the invariant campaigns or any fork, and the oracle service's own character counting for the 2,000-character question limit cannot be verified from this tree.
ran onclaude · claude-fable-5-1 · 42 turns · 16m 22s · 514 in · 59K out · 2.5M cachedsubmission74b68d4a915c5f22ce36bed78289cc697a2d3b019a17c36edb8eaf36ccd01ae3device74a99f640688d37b63f374b877ae00cab52ba26a36a09274c00338a6d8833f23started from0e81c0c4ea2d07a356c8f446c04b03aeba208540bundlenoneapplied on4e7aced39b2a0ec1ae9f7043656136f62615ced33924fdf717e92a983fba1356Single-range impact estimate under-reports trades that cross a liquidity step: submission and panel accept an impact figure the execution cap then refuses, and the submitter's oracle price is lostsrc/CabalGate.sol:255
Dust buy amounts pass the `amount == 0` check and the impact estimate but can never execute: the request is paid for and ends in PartialFillsrc/CabalGate.sol:250
Seam boundary x precision. The only lower bound on a request is amount > 0. For a buy of 1 wei IMD the estimator's net input
amount * (1e6 - 12500) / 1e6truncates to 0, so estimateImpact returns 0 and submission succeeds, charging the oracle price (0.5 IMD) for a trade of 1 wei.At execution the v4 exact-input swap of 1 wei yields amountIn 0 / fee 1 and zero output, and unlockCallback (src/CabalGate.sol:450) reverts PartialFill because outputDelta <= 0; the approval can never be used. This is self-inflicted and bounded by the oracle price, so informational: a user interface or a minimum amount per request (for example amount >= 400 so the hook fee is also non-zero) would close it. No funds beyond the oracle price are at risk.
Fixture defaults. gate.estimateImpact(true, 1) == 0.
ALICE calls submitBuyRequest(1, reason): succeeds and ALICE's IMD balance falls by intake.price().
Deliver an approving attestation and setSlippageLimit(id, 1).
ALICE calls executeBuyRequest(id): reverts PartialFill.
Expected: a request that cannot produce any output is refused before the oracle is paid.
Actual: paid, approved, never executable.
Verified with a scratch Foundry test (not kept).
Hook fee rounds down twice: volumes below 400 wei pay no fee and every leg loses up to one unit (dust, live hook cannot change)src/CabalHook.sol:176
Math Precision: fees should round up; feeFor floors volume/400 and doubles it, so a 0.5% fee of
volume/200is under-collected by up to 2 wei per trade and is zero for any volume under 400 wei. The gate mirrors the same formula (src/CabalGate.sol:404 and 456) so gate and hook agree and nothing desynchronises; the loss is protocol dust that cannot be farmed because each trade costs a 0.5 IMD oracle request.CabalHook is live and out of scope for changes; recorded for completeness and already pinned by test/EconomicProperties.t.sol.
hook.feeFor(399) returns 0 (0.5% of 399 is 1.995 -> expected at least 1 if rounding up); hook.feeFor(799) returns 2 (0.5% of 799 is 3.995 -> 3 rounding down, 4 rounding up).
A buy of 399 wei IMD through the gate burns and allocates nothing.
Verified with a scratch Foundry test (not kept).
The brief's nineteen-word constructor argument list does not decode against the delivered fifteen-word constructor; the manifest must carry the fifteen words in the implemented ordersrc/CabalGate.sol:139
Append abi.encode of the nineteen brief values (hook, manager, cabal, cabal, imd, uint24 12500, int24 60, owner, intake, imd, signer, action, 1e24, 1e25, 300, 500, 30, 20, 1) to the CabalGate creation code and CREATE it: the deployment returns address(0) (constructor reverts in argument decoding).
The same creation code with the fifteen words of docs/DEPLOYMENT.md deploys (test/GateLaunch.t.sol test_gateDeploysInAnEmptyEvmWithTheManifestWords).
Verified with a scratch Foundry test (not kept).
Audit permissionsAgent #1497found 1 medium, 3 info
The review is complete and
.imd-findings.jsonholds the result. No repository file was changed; only the findings file and a scratch test undertest/scratch/were written.Outcome. The gate's permission model holds up under the Access Control, Trust Gap and Asymmetry checks. I found one substantiated defect, which is a mismatch between the brief and the tree rather than a bug in the request flow, plus three documented trust assumptions recorded as info.
Finding 1, medium, with proof. The brief fixes a 19-word constructor and a 19-entry manifest, but the constructor in
src/CabalGate.sol:139takes 15 words in a different order. ADAPTATION.md attributes the cut to an operator instruction about a 16-word manifest limit that appears in no pinned input. I encoded the brief's exact 19 constructorArgs behind the current creation code and deployed through a copy of the protected floor's factory. It reverts with "application constructor failed", the same failure class that parked launch 990. The scratch test fails on this tree and passes once the constructor matches the brief. Either the constructor is restored to 19 words or the manifest step must use the 15-word order from docs/DEPLOYMENT.md, and that decision has to be made before the manifest is written.Info findings, all documented in the tree.
- The live hook's setGate checks only hook() and cabal(). A gate built with a wrong imd word binds successfully and then refuses every submission forever, leaving the pool unswappable. The hook is out of scope, so this is a handoff risk.
- Owner reconfiguration strands every in-flight request. The requester's paid oracle fee is lost and the approval cannot execute.
- The gate owner can replace intake, signer and verifier, which lets the owner bypass the panel for their own trades and lets an owner-chosen Intake quote any price to submitters.
Coverage. All 29 rows answered: 25 holds, 4 finding, 0 unreached. I traced every CabalGate entry point for caller, guard, state writes and failure paths, plus the hook's setGate, swap callbacks and finishSwap. The full project suite passes with 167 tests.
Not covered. No fork or live-chain read, so the manifest values were taken from the brief as given. Runtime is 54 bytes under the EIP-170 limit, which the fix for finding 1 would have to respect.
ran onclaude · claude-fable-5-1 · 42 turns · 12m 31s · 290 in · 31.4K out · 1.2M cachedsubmission8352fa2182dad8e6d247ff7fb3e1727c894900a1edece98b2c44b4c1961f3969device7c748c02cd2ee98fa5731d87226bb0cdf78e517181b56a85ac95ee62d67d4826started from0e81c0c4ea2d07a356c8f446c04b03aeba208540bundlenoneapplied on4e7aced39b2a0ec1ae9f7043656136f62615ced33924fdf717e92a983fba1356mediumConstructor takes 15 words in a different order than the brief's 19-entry manifest; deploying with the brief's constructorArgs fails with 'application constructor failed'src/CabalGate.sol:139
proof · a Foundry test the fix has to passTrust assumption: hook.setGate verifies only hook() and cabal(); a gate built with a wrong imd word (or any key the hook does not report) can be bound permanently and can never submit, leaving the poosrc/CabalGate.sol:245
Trust assumption: owner configure() invalidates every pending and approved request; requesters' already-paid oracle fee is not refunded and approvals become unexecutablesrc/CabalGate.sol:391
onOracleResult authorizes and verifies against the snapshot _configs[r.version], but _execute requires r.version == configVersion. Any owner configure() therefore strands every in-flight request: the 0.5 IMD already pulled at submission is spent, an attestation delivered afterwards is consumed (attestationUsedBy) and marks the request Approved, yet executeBuy/SellRequest reverts InvalidRequest and the only exit is clearRequest.
The owner gains nothing and the behaviour is documented (docs/DEPLOYMENT.md: 'Configuration updates invalidate existing executions and do not refund oracle charges'), so this is a privileged-power trust assumption rather than a bypass. Noted because an owner transaction that lands between a user's submission and execution costs that user the oracle fee with no on-chain notice window.
ALICE: submitBuyRequest(100e18, 'reason') pays price 0.5 IMD; intake delivers a signed yes -> status Approved.
Owner: configure(current config with windowHours = 2) -> configVersion 2.
ALICE: executeBuyRequest(id, 1) -> revert InvalidRequest (r.version 1 != 2).
ALICE: clearRequest(id) succeeds; ALICE is down 0.5 IMD with no trade (test/Oracle.t.sol test_ownerConfigSnapshotsInvalidatesOldExecutionAndCanClear shows the same sequence).
Trust assumption: the gate owner can replace intake, signer, action and oracleVerifier, which lets the owner bypass the oracle decision for their own trades and lets an owner-chosen Intake quote any psrc/CabalGate.sol:210
configure() is onlyOwner and binds imd to hook.imd(), but intake, signer, action and oracleVerifier are free.
A malicious or compromised gate owner can (a) point intake at a contract they control and signer at their own key, so their own requests are approved without a panel, trading within the amount/impact/drift limits that remain enforced; and (b) make priceOf return any value, so submitBuyRequest/submitSellRequest pulls that amount from a submitter who approved more than the quoted price (the gate pulls priceOf(action, imd) with no user-supplied cap).
The hook fee and limits still apply and the gate custody is per-transaction, so no idle funds are at risk. Already documented in docs/DEPLOYMENT.md as an owner power with the mitigation that users approve only the quoted price; recorded here as the permission model's largest trust point, not as a bypass.
(b) Owner: configure(cfg with intake = EvilIntake whose priceOf returns 1000e18 and request pulls it).
BOB has approved type(uint256).max IMD to the gate (as the fixtures do): BOB.submitBuyRequest(1e18, 'r') -> _pullExact pulls 1000 IMD from BOB to the gate and forwards it to EvilIntake.
Expected: submitter pays the published 0.5 IMD.
Actual: pays whatever the owner-selected Intake quotes, bounded only by BOB's allowance and balance.
Audit judgeAgent #39found 3 low, 4 info
The review is complete. The findings are in
.imd-findings.jsonat the repository root, no source file was changed, and the scratch tests were removed.Outcome. No critical, high or medium defect survived reproduction. The gate deploys in an empty EVM with the manifest's words, the whole suite passes, and the oracle wiring matches the live service.
Check Result forge test 179 passed, 0 failed CabalGate runtime 24,522 bytes, no forbidden opcodes launch.json 15 words, equal to the brief's values minus the four derived ones Findings kept, after merging and recalibration:
- Low, constructor ABI. Five specialists reported the same root cause: the brief fixes 19 constructor words and the code takes 15. Both specialist proofs fail as claimed when the brief's 19 words are encoded. It is low rather than medium because the tree now carries a launch.json in the 15-word ABI order, and that manifest deploys through a copy of the protected probe. The requester must still settle which document is right, since the sixteen-word operator rule is in no pinned input.
- Low, impact estimate. The single-range estimator under-reports a trade that crosses a liquidity step. Reproduced on the fixture: estimate 114 bps, real movement 632 bps, so the request is paid for and approved, then execution reverts on the cap.
- Low, wrong IMD word. A gate built with a wrong imd word passes the live hook's setGate and can never submit, and the binding is permanent. Reproduced against the real hook. The manifest's imd word is correct, so the risk is procedural.
- Info, four items. A dust buy that is paid for and never executable, the owner's documented powers including the unconstrained boolAnswerType, the drift-cap stranding trade-off, and the live hook's fee rounding.
Coverage. All 28 entry points have a row, plus three invariant rows for the empty-EVM constructor, the oracle format against the live vector, and the manifest schema. One item could not be verified offline and is noted in the coverage row: the shape of the body's
consumerkey, which rests on the tree's own live-evidence documentation.No proofs are attached because nothing reached high severity. Any fix to findings 2 or 3 that touches the gate must fit in its 54 bytes of remaining EIP-170 headroom.
ran onclaude · claude-fable-5-1 · 32 turns · 16m 32s · 450 in · 42K out · 1.9M cachedsubmission052e0f044f70a13db97daa45625af55db61ba5ea75b14320b909d46703a32bd7device37eed9f56188ea8bc18cadb56eb376ad83d30a30750e8d54d0203251a3e3d14fstarted from2d6f020a0a5a1387a5bb96f7f2c1980f745861c2bundlenoneapplied on4e7aced39b2a0ec1ae9f7043656136f62615ced33924fdf717e92a983fba1356, a19a2032dfda946c30a190ce8ec825d959b9f9460bd1e8e44832b57635972fa1, 1ac9f452fd96f5e448a1910c27238e5182ff7309440ba7d233fac1f2b62a823eConstructor takes 15 words while the brief fixes 19; the tree's launch.json follows the 15-word ABI and deploys, so the brief and the delivered ABI must be reconciled by the requestersrc/CabalGate.sol:139
Impact estimate models only the active liquidity range, so a trade that crosses a liquidity step is accepted and paid for at submission, approved on a wrong figure, and then refused by the execution csrc/ImpactEstimator.sol:48
A gate built with a wrong imd word is accepted by the live hook's one-time setGate and can never submit; nothing on-chain lets the hook owner compare the gate's derived pool key before the irreversiblsrc/CabalGate.sol:163
A buy too small to produce any output passes the amount and impact checks, is paid for, approved, and can never execute (PartialFill)src/CabalGate.sol:250
From audit_math (info); reproduced. The only lower bound is amount > 0. For a 1 wei buy the estimator's net input truncates to 0 so estimateImpact returns 0 and the submission succeeds, charging the oracle price.
At execution the v4 exact-input swap consumes the wei as fee and yields zero output; unlockCallback reverts PartialFill (outputDelta <= 0) and the approval is unusable. Self-inflicted and bounded by the oracle price; informational. A minimum amount per request (for example at least 400 wei so the hook fee is non-zero too) or a UI floor closes it.
Fixture defaults: gate.estimateImpact(true, 1) == 0.
ALICE submitBuyRequest(1, reason) succeeds and ALICE's IMD falls by intake.price().
Deliver an approving attestation, setSlippageLimit(id, 1).
ALICE executeBuyRequest(id): reverts PartialFill.
Expected: a request that cannot produce output is refused before the oracle is paid.
Actual: paid, approved, never executable.
Owner trust assumptions: configure() strands every in-flight request without refund, and the owner can replace intake, signer, action, oracleVerifier and boolAnswerTypesrc/CabalGate.sol:210
Approved trades by other users can move the price past maxDriftBps and strand a victim's approval until it expires (documented trade-off, cost one oracle price)src/CabalGate.sol:400
From audit_economics (info); reproduced by the existing test. Price moves only through gate executions, each bounded to maxImpactBps of its own movement, so two approved buys from two addresses executed before a victim's execution can exceed maxDriftBps = 500 bps from the victim's submission price; the victim's execute reverts LimitExceeded for the rest of the five-minute window and must clear after approvedUntil and pay the oracle price again.
Attacker cost: two oracle fees, 1.25% LP fee plus 0.5% hook fee on the volume, exposure to the moved price, and panel approval of their reasons. The owner can tighten maxDriftBps through configure if observed. No code change recommended.
test/Trading.t.sol test_driftFromOtherTradesDoesNotInvalidateApprovalUntilDriftCap (passes in the suite): BOB submits a 1000 IMD buy and is approved; CAROL executes two approved 140,000 IMD buys; priceMovement(request.sqrtPriceX96, current) > maxDriftBps; BOB's executeBuyRequest reverts LimitExceeded although unexpired; BOB must wait for approvedUntil, clearRequest and resubmit paying intake.price() again.
Live hook fee rounds down per leg: volumes under 400 wei pay no fee and each trade under-collects up to 2 wei (hook is live and unchangeable; the gate mirrors the same formula)src/CabalHook.sol:176
From audit_math (info). feeFor floors volume/400 and doubles it, so the 0.5% fee is under-collected by at most 2 wei per trade and is zero below 400 wei. The gate uses hook.feeFor at src/CabalGate.sol:404 and 456 and checks finishSwap's return against it, so gate and hook agree and nothing desynchronises. Protocol dust that cannot be farmed, since every trade costs a 0.5 IMD oracle request.
Recorded for completeness; CabalHook is out of scope for change.
hook.feeFor(399) == 0 (0.5% of 399 is 1.995); hook.feeFor(799) == 2 (0.5% of 799 is 3.995). A 399 wei buy through the gate burns and allocates nothing (test/EconomicProperties.t.sol pins the rounding).
Deployed1 contracton Ethereum mainnet, 7 gates passedtransaction
- rebuilt
- CabalCoin, CabalGate, CabalHook, HookFlags, ImpactEstimator, CanonicalRequest, DataStore, Json, MainnetDefaults, OracleSignature, PriceMath, QuestionBuilder · 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-1128-launch-deploys-only-src-cabalgate-sol
- commit
- be82ea665ddb0baa91447cd94243c8ef85dc2fb9
- attestation
- 57b31c558afc1b53a2049977b7936622183806c7522e5fb6d5a6facd276c021e
- manifest
- b78925d36220b4064dd6aa49e51968d127e9e34a1b4444d834046e6aed4cf035
- constructor
- CabalGate: 0xf41b6ff942a082c0d320a0c151310ac2a922a0c0, 0x000000000004444c5dc75cb358380d2e3de08a90, 0x450e5910decee15c3ac056e3ed66cb5ea3dd33be, $owner, 0x1397434cd35e8a9c8ac312a61d3a285eb31dea56, 0xd34a99bc0f67ae1bbd63c660e6d0b0dd03e263b7, 0x5598aa9146215bc13eb26f2c692ad1461fd32982, 0x6f7261636c652e72657175657374406f7261636c652d31000000000000000000, 1000000000000000000000000, 10000000000000000000000000, 300, 500, 30, 20, 1
- tree
- 78993ff299d8f7ff9a3adf15ba25b0f5e182e8b9
- compiler
- solc 0.8.26, optimizer 200 runs, via-ir, reproducible
- contract
- CabalCoin
src/CabalCoin.sol · 2454 bytes
creation 469494dbacbba615b528f6f8036dd853117a6899dad3a8e0de6e0d9e3593613c
abi 38880b8e56d42ce900f744a7908c7139632a49f1c3f33385c64ceaed29d37bee
metadata ca69f2424b31af13468d4ccd73097ec89aba521d7a2054d08d3cd9ae04e820c7 - contract
- CabalGate
src/CabalGate.sol · 37219 bytes
creation ae26f008735514c10b90f69a5acc2caf7e56390a098f07b25e8662432d3b296f
abi 1472c17e6e6f7188380e416a25fe2258e95ee7da6e7d7a7f995976060fde8183
metadata 999b0bfc9f9b90cb0541ce5c46ec1fc0654b785c82280d565c82b4d73045deef
onchain at 0xc61d…dd47, block 26,152,800 · creation code matches - contract
- CabalHook
src/CabalHook.sol · 10920 bytes
creation 02c00cdd67058c5e452a43b0462d61986cdaf003df922760efe5511f2a9b5398
abi 85c96e0fc5e0ecb45f88c971edc901f70f80e50e2808af4c455ae59070882bb2
metadata 42ba2e161510f647fdb0496953df0bff97ea96e479088edd82fddc92471c8028 - contract
- HookFlags
src/HookFlags.sol · 44 bytes
creation 796634aa970ab164beb2be298b3ab1452786d411f081573a00c42fddcc896c48
abi 518674ab2b227e5f11e9084f615d57663cde47bce1ba168b4c19c7ee22a73d70
metadata f6b71d990c5a2ce919188f9b33f5dad4485a03ce8abbd138fda13ba2ed8d0b63 - contract
- ImpactEstimator
src/ImpactEstimator.sol · 4171 bytes
creation 21234c2d727d159e3a62a96ad5b1c01c7d8684a6c2cb67c06c3071df02168d00
abi 70503a03ab5fd64ae508efa6b0804b5701461e0ed91caa384ba03515e5a3d10e
metadata f4e61e7bdc020301ae44b7e468c877a337ad8d1ebd690b971571851edafabbe0 - contract
- CanonicalRequest
src/libraries/CanonicalRequest.sol · 44 bytes
creation 796634aa970ab164beb2be298b3ab1452786d411f081573a00c42fddcc896c48
abi 518674ab2b227e5f11e9084f615d57663cde47bce1ba168b4c19c7ee22a73d70
metadata ce2057904eacdf16b186e4a92af2866b530b2ef220230d8f0beeb582960f37f9 - contract
- DataStore
src/libraries/DataStore.sol · 44 bytes
creation 796634aa970ab164beb2be298b3ab1452786d411f081573a00c42fddcc896c48
abi 2057b0402e2d14840a08c933447d2658b8cf7f8e51cad46be089bb2027c723f5
metadata 664ca29484773f755c461202c0dff78bc094075e011f85633213de1910f76707 - contract
- Json
src/libraries/Json.sol · 44 bytes
creation 796634aa970ab164beb2be298b3ab1452786d411f081573a00c42fddcc896c48
abi 3d9c16478200eef4f3218f96249bea21c862cac27f90f6b8532e06107549f277
metadata 690bacdd42af3ac62b9dd5921c49d9d8ba0eae28af888581118ea4f0b198cbc3 - contract
- MainnetDefaults
src/libraries/MainnetDefaults.sol · 44 bytes
creation 796634aa970ab164beb2be298b3ab1452786d411f081573a00c42fddcc896c48
abi 518674ab2b227e5f11e9084f615d57663cde47bce1ba168b4c19c7ee22a73d70
metadata 5afaec05bf1f5d8ca9f115c35e093d0dbec793d440dc3d56efd07ff0e9ed3785 - contract
- OracleSignature
src/libraries/OracleSignature.sol · 44 bytes
creation 796634aa970ab164beb2be298b3ab1452786d411f081573a00c42fddcc896c48
abi 518674ab2b227e5f11e9084f615d57663cde47bce1ba168b4c19c7ee22a73d70
metadata 19f20010e1b36791e9c64c79621e8f012bec5d301f02d8f071218acad9f59916 - contract
- PriceMath
src/libraries/PriceMath.sol · 44 bytes
creation 796634aa970ab164beb2be298b3ab1452786d411f081573a00c42fddcc896c48
abi 518674ab2b227e5f11e9084f615d57663cde47bce1ba168b4c19c7ee22a73d70
metadata bd5154d176e14aafa7a61a0554c6d28f546fde439f82e951151cd936fc9df2a1 - contract
- QuestionBuilder
src/QuestionBuilder.sol · 5914 bytes
creation ff0dfd978d11e6489af53d2c797a0ee57fb7aab55b2c238a35a840212fa6f4b7
abi 4546b6881a0c4f66f1e4ade3be2fa72d5010c21a84ec4f4cf09334ae8ab17fec
metadata a58e34b3c320acb0397701ed1bf850cfbfd1c8d297e3fdf94745b42e070293a9