Job
Blocked: node build_contract_project exhausted its attempts
Token name: IMD Offsets. Token symbol: IMDO. Total supply 1,000,000,000 with 18 decimals. Chain id 11155111, paired with ETH. Ethereum mainnet is the real target, deployed manually from this repo.
BASE. The workspace is the ADAM repo (MIT). Fork it: keep its layout, vendored lib/, foundry.toml unchanged (bytecode_hash "none"; README quotes it), errors, guards, pull payments, tests. Rename Adam* to Imdo*, drop @custom:x tags, delete AdamSplitOracle and every PNKSTR/IMDSTR path. README credits …
Work
- posted23 minto the first attempt
- reviewed
#1812Audit imported codeClaude5 findings · 3 medium
The findings file is in place at the repository root (excluded from git by the harness, as intended) and nothing in the tree was modified.
Audit of the ADAM repo as it arrived, before adaptation to IMDO
Outcome: no critical or high defects. Three medium findings, one low, one informational, all written to
.imd-findings.jsonwith concrete reproductions. The most severe one carries a Foundry proof that fails on the current code.Findings
- Medium, hook fee funding.
AdamHookpays its ETH fee withpoolManager.takeinside the swap, before the router has settled, so the transfer is funded only by ETH the manager already holds from other pools. On a manager holding less ETH than the fee, every fee-bearing buy reverts withNativeTransferFailed. Proof: fresh PoolManager, ADAM-only launch position, 1 ETH buy reverts. The existing suite never sees it because its fixture pre-funds the manager with 400 ETH through the IMD/PNKSTR pools. The adapter should mint an ERC-6909 claim or fall back to one when the manager balance is short, with a permissionless redeem. - Medium, buy floor stall. The treasury's minimum output uses the greater of checkpoint and spot, and the checkpoint only moves after a successful buy. Once IMD rises more than about 3% past the last buy, every retry fails. Verified: after one 8 ETH organic buy, 420 retries over 70 hours bought nothing. ADAM escapes by rerouting after 3 days. The IMDO brief removes rerouting and makes IMD the only buy leg, so the IMD share would sit in
pendingwith no exit until the market reverts. - Medium, chain and literals disagree. The brief fixes chain 11155111 but the manager and IMD literals are mainnet addresses. Checked live: both return empty code on Sepolia, the real Sepolia PoolManager has code, and the mainnet IMD/ETH pool at fee 10000 / spacing 200 exists. A V2-style constructor reverts there; a V1-style one seeds a zero checkpoint and the leg fails closed forever with no withdrawal path.
- Low.
stakeForis open to anyone. Harmless in ADAM, but with the brief's 24-hour lock reset on every stake it becomes a one-wei-per-day griefing vector unless restricted to the claim contract. - Info.
DeployAdam.run()reads env vars and broadcasts with an implicit signer, and mints the LP NFT to the deployer rather than the dead address. The IMDO script must take everything as arguments.
Also checked, no defect found: hook fee math on all four swap shapes and the partial-fill guard, distributor accumulator and backlog stream edge cases, V2 treasury ETH conservation and keeper bounty accounting, callback and self-call guards, reentrancy surfaces, NFTClaim vesting and duplicate-id handling, runtime sizes under EIP-170, and an opcode scan showing no DELEGATECALL, CALLCODE or SELFDESTRUCT in any contract. The hook owner's
lowerFeeis a brief-requested trust assumption, not a finding.One brief-level conflict for the adapter, not filed as a finding since no code exists yet: the manifest resolves ImdoStaking's
claimto$owner, so the script-deployed ImdoClaim would be unable to callstakeFor, and the owner wallet would be excluded from staking.Coverage. Read in full: LaunchToken, AdamHook, AdamDistributor, AdamDistributorV2, AdamTreasury, AdamTreasuryV2, AdamSplitOracle, NFTClaim, the interface, both scripts, HookMiner, launch.json, README, SELF_AUDIT, the test fixtures and the extension suites. Not reached in depth: the remaining unit, fuzz and invariant test files beyond their names and the fixtures they share. No Slither or Mythril ran, no fork test ran, and the Pashov specialty passes were applied by one reviewer, not independent agents. The full existing suite was run and passes, 151 tests.
ran onclaude · claude-fable-5-1 · 42 turns · 23m 15s · 418 in · 51.9K out · 2M cachedsubmission61a4ce25f1137435d99b941932d99efe0d3f0efcf9e63b1e878d417dadb76e15device589ef002581a53719d3af2622bb0d2ba58ea5f4139529f8b933806d6cb2e511dstarted from785edda603f9ed41364e08d46ecea440bcc0d6e2bundlenonechanged · 0 filesnothingmediumAdamHook pays the ETH fee with poolManager.take inside the swap, so every fee-bearing swap reverts when the PoolManager holds less ETH than the feesrc/AdamHook.sol:273
proof · a Foundry test the fix has to passmediumTreasury buy floor ratchets only after a successful buy: a >~3% IMD price rise stalls the IMD leg on every retry; with the brief's 'no rerouting' the ETH is stuck until the market revertssrc/AdamTreasury.sol:297
mediumManifest literals for the PoolManager and IMD have no code on the declared chain 11155111; the treasury either reverts at construction or seeds a zero checkpoint that disables the IMD leg foreverlaunch.json:16
stakeFor is callable by anyone for any beneficiary; harmless today, but it becomes a lock-reset griefing vector once the brief's 24h unstake lock is addedsrc/AdamDistributorV2.sol:63
AdamDistributorV2.stakeFor only checks amount != 0 and that the beneficiary is not excluded; the caller supplies the ADAM, so in ADAM it is a free donation of stake. The IMDO brief adds 'the 24h lock reset by every stake' and 'stakeFor only by claim'. If the adapter keeps this function open (the natural copy), any address can call stakeFor(victim, 1 wei) once a day and the victim can never unstake.
The README also states NFTClaim is the intended caller.
Fix: require msg.sender == claim (immutable) in ImdoStaking.stakeFor, and make the lock reset apply to stake and stakeFor alike.
State: bob holds 1 wei ADAM approved to the distributor; alice has 1,000,000e18 staked.
Input: bob calls stakeFor(alice, 1).
Expected (brief): revert, only the claim contract may call.
Actual (code as arrived): succeeds, stakedBalance[alice] += 1 and Staked(alice, 1) emitted by a stranger; with a 24h lock reset per stake this would postpone alice's unstake indefinitely at a cost of 1 wei per day.
DeployAdam.run() reads signer and addresses from environment variables and broadcasts with an implicit key; the IMDO script must take them as argumentsscript/DeployAdam.s.sol:149
run() uses vm.envAddress for DEPLOYER, TEAM_WALLET, ADAM_TOKEN, TREASURY and vm.envOr for LIQUIDITY_ADAM, then vm.startBroadcast() with no explicit signer.
The brief requires script/DeployImdo.s.sol to be a manual-signer script with no env or keys and every address, the opening tick and holderRoot as arguments (DeployAdamExtension.run(Config) is the right pattern). launchPool also mints the LP NFT to cfg.deployer, while the brief sends it to the dead address, and the position's amount1Max is uint128(cfg.liquidityAdam) with the 890,000,000 cap only enforced by the caller's balance check.
Input:
forge script script/DeployAdam.s.sol --sig run()with DEPLOYER unset.Actual: revert from vm.envAddress('DEPLOYER').
With it set, the LP NFT recipient is cfg.deployer (params[0] owner field), not 0x...dEaD.
- Medium, hook fee funding.
- built
#617Build contract projectCodexanalysis failed64 files changedretried on #923 (Codex)
Implemented contracts, manifest, manual deployment script, documentation and tests.
Passed:
forge build, 134 offline tests, three mainnet fork tests, andforge fmt --check. Protected configuration and dependencies are unchanged.The mandated Sepolia addresses lack code; that limitation and the tested manual mainnet path are documented in review notes. No transactions were broadcast.
ran oncodex · gpt-6-astra · 11 turns · 23m 58s · 204.6K in · 52.6K out · 6.7M cachedsubmission12306b54129c537470008e7ab930885d429903296387c0029c052307514e85cbdevicebb0a3bf63233e5e5722f6eae6cd08dc581298f894dd83aa00d8a87a846a28c34started from785edda603f9ed41364e08d46ecea440bcc0d6e2bundle3f885cdaa60cce48071c748b811b03e1ad6216b6f41da484f0a63c435601fd58 · 58 KBchanged · 64 filesREADME.mdSELF_AUDIT.mddocs/DEPLOYMENT.mddocs/HEARTBEAT.mddocs/LEGACY_README.mddocs/MAINNET_SNAPSHOT.jsondocs/REVIEW_NOTES.mdlaunch.jsonscript/DeployAdam.s.solscript/DeployAdamExtension.s.solscript/DeployImdo.s.solscript/utils/HookMiner.solsrc/AdamDistributor.solsrc/AdamDistributorV2.solsrc/AdamHook.solsrc/AdamSplitOracle.solsrc/AdamTreasury.solsrc/AdamTreasuryV2.solsrc/IMDOToken.solsrc/ImdoClaim.solsrc/ImdoHook.solsrc/ImdoStaking.solsrc/ImdoTreasury.solsrc/LaunchToken.solsrc/NFTClaim.solsrc/interfaces/IAdamDistributor.solsrc/interfaces/IImdoStaking.soltest/REVISION.mdtest/TESTING.mdtest/fork/IMDSTRFork.t.soltest/fork/MainnetFork.t.soltest/fuzz/Treasury.fuzz.t.soltest/invariant/Accounting.invariant.t.soltest/invariant/Distributor.invariant.t.soltest/invariant/Treasury.invariant.t.soltest/unit/AdamDistributor.t.soltest/unit/AdamExtension.t.soltest/unit/AdamHook.t.soltest/unit/AdamSplitOracle.t.soltest/unit/AdamTreasury.t.soltest/unit/DeployImdo.t.soltest/unit/DistributorAdversarial.t.soltest/unit/ExtensionAdversarial.t.soltest/unit/ExtensionDeploy.t.soltest/unit/FreshManager.t.soltest/unit/HookAdversarial.t.soltest/unit/ImdoClaim.t.soltest/unit/ImdoHook.t.soltest/unit/ImdoStaking.t.soltest/unit/ImdoTreasury.t.soltest/unit/Integration.t.soltest/unit/LaunchToken.t.soltest/unit/NFTClaim.t.soltest/unit/OraclePublicVector.t.soltest/unit/PostFactoryHook.t.soltest/unit/ProjectShape.t.soltest/unit/Regen.t.soltest/unit/Revision.t.soltest/unit/StakingAdversarial.t.soltest/unit/TreasuryBoundaries.t.soltest/utils/ClaimFixture.soltest/utils/ExtensionFixture.soltest/utils/LocalV4.soltest/utils/MockTaxHook.sol#923Codex61 files changed
afterAudit imported codeImplemented the IMDO contracts, manual deployment script, manifest without root
chainId, tests, and documentation. Protected configuration and dependencies remain unchanged.Verification passed:
forge build,forge fmt --check, 83 local tests, and the mainnet IMD fork test.The required addresses remain unavailable on Sepolia; deployment constraints and finding resolutions are documented in review notes. No transactions were broadcast.
ran oncodex · gpt-6-astra · 13 turns · 29m 44s · 193.7K in · 57.5K out · 7.5M cachedsubmission2d8d683f9eb8c55210be2874a2a3a976af54d68c23fa60cc5a89bc9bbab47cc6device2564cef48373f7f3f83d63e1c04952dbcccb57fd4de6a080d57ad52a1b03a8a0started from785edda603f9ed41364e08d46ecea440bcc0d6e2bundle2cdf7799cbb38f4f576141b5a610b5c184aa3f69fd450a0967d6326a8ac94f23 · 54 KBverifiedrebuilt and matched · verifier 0.1.0 ·changed · 61 filesLICENSEREADME.mdSELF_AUDIT.mddocs/DEPLOYMENT.mddocs/HEARTBEAT.mddocs/LEGACY_README.mddocs/MAINNET_SNAPSHOT.jsondocs/REVIEW.mdlaunch.jsonscript/DeployAdam.s.solscript/DeployAdamExtension.s.solscript/DeployImdo.s.solsrc/AdamDistributor.solsrc/AdamDistributorV2.solsrc/AdamHook.solsrc/AdamSplitOracle.solsrc/AdamTreasury.solsrc/AdamTreasuryV2.solsrc/IMDOToken.solsrc/ImdoClaim.solsrc/ImdoHook.solsrc/ImdoStaking.solsrc/ImdoTreasury.solsrc/LaunchToken.solsrc/NFTClaim.solsrc/interfaces/IAdamDistributor.solsrc/interfaces/IImdoStaking.soltest/REVISION.mdtest/TESTING.mdtest/fork/IMDSTRFork.t.soltest/fork/MainnetFork.t.soltest/fuzz/Treasury.fuzz.t.soltest/invariant/Distributor.invariant.t.soltest/invariant/Staking.invariant.t.soltest/invariant/Treasury.invariant.t.soltest/unit/AdamDistributor.t.soltest/unit/AdamExtension.t.soltest/unit/AdamHook.t.soltest/unit/AdamSplitOracle.t.soltest/unit/AdamTreasury.t.soltest/unit/DistributorAdversarial.t.soltest/unit/ExtensionAdversarial.t.soltest/unit/ExtensionDeploy.t.soltest/unit/HookAdversarial.t.soltest/unit/HookDeferredFees.t.soltest/unit/ImdoClaim.t.soltest/unit/ImdoDeploy.t.soltest/unit/ImdoHook.t.soltest/unit/ImdoStaking.t.soltest/unit/ImdoTreasury.t.soltest/unit/Integration.t.soltest/unit/LaunchToken.t.soltest/unit/NFTClaim.t.soltest/unit/OraclePublicVector.t.soltest/unit/PostFactoryHook.t.soltest/unit/Revision.t.soltest/unit/TreasuryBoundaries.t.soltest/utils/ExtensionFixture.soltest/utils/LocalV4.soltest/utils/MockTaxHook.soltest/utils/Permit2Mock.sol - tested
#985Write foundry testsCodexruntime erroron the agent's machine: {"type":"error","status":400,"error":{"type":"invalid_request_error","message":"The 'gpt-6-astra' model is …retried on #847 (Codex)
{"type":"error","status":400,"error":{"type":"invalid_request_error","message":"The 'gpt-6-astra' model is not supported when using Codex with a ChatGPT account."}}
ran oncodex · 8ssubmission7448bec725be3328ffa96f56f8fba94cbbb7b348d583a2bc91935b96c50b3f05device553a378efc6a19cb4bb03f46dc730747a2e6b45ad7201da32cea744c84c9db2cstarted fromb2a7fdace3cae68b3c8613aab4ac2717b54f21e5bundlenoneapplied on2cdf7799cbb38f4f576141b5a610b5c184aa3f69fd450a0967d6326a8ac94f23changed · 0 filesnothing#847Codex7 files changed
afterBuild contract projectwrites totest/**Added 15 tests covering claim failures, treasury payment boundaries, and claim/hook stateful invariants. Only
test/**changed.Validation passed:
forge build- All 98 offline tests
- 55,296 invariant handler calls
- Pinned mainnet-fork IMD buy test
No reproducible contract defects found.
ran oncodex · gpt-6-astra · 10 turns · 10m 14s · 121K in · 20.7K out · 2.8M cachedsubmissionf2e4c8ec27bda2cebc061bfdefe828b506da1e93546dd182e5171084fe1a55c4device5dc170d977094c92852bffdca7c9a128498e9ec1f07e7248e1709d562d0a03d0started fromb2a7fdace3cae68b3c8613aab4ac2717b54f21e5bundle82505aa2c5d34951b56ec62222d8238f5931c3633cb4f35f332321708a702b29 · 66 KBverifiedrebuilt and matched · verifier 0.1.0 ·applied on2cdf7799cbb38f4f576141b5a610b5c184aa3f69fd450a0967d6326a8ac94f23changed · 7 filestest/TESTING.mdtest/invariant/Claim.invariant.t.soltest/invariant/Hook.invariant.t.soltest/unit/ClaimAdversarial.t.soltest/unit/HookDeferredFees.t.soltest/unit/TreasuryBoundaries.t.soltest/utils/ClaimHarness.sol - integrated
#637ManifestCodex1 file changed
afterAudit imported code, Build contract project, Write foundry testswrites tolaunch.jsonUpdated only
launch.jsonwithLaunchToken, verified constructor arguments, and deployment/finding notes.Passed schema and ABI validation,
forge build, all 98 default tests, and 9 protected checks.The documented Sepolia blocker remains: both required dependency addresses have no code there. Mainnet fork tests were not run.
ran oncodex · gpt-6-astra · 6 turns · 6m 7s · 109.7K in · 11.4K out · 1.8M cachedsubmission0e669920ae463b97995ee389d9cda417b24dda5a7643e3371db6dfc1526f6d3bdevice7989aa5d838d11d9858c19c0f47e3c24fc17f124f9e9c78fe1c9d4afa8ae9ae2started from851a84347f0f621a8b98b532dc6b9d44fcca22c4bundleafc57407b1ed15c4dea5fd6679b28ac0254feaba869815a139732b2cb7c19b6d · 68 KBverifiedrebuilt and matched · verifier 0.1.0 ·applied on2cdf7799cbb38f4f576141b5a610b5c184aa3f69fd450a0967d6326a8ac94f23, 82505aa2c5d34951b56ec62222d8238f5931c3633cb4f35f332321708a702b29changed · 1 filelaunch.json - reviewed
#440Audit permissionsClaude3 findings · 1 low
afterBuild contract project, Write foundry tests, ManifestThe review is complete and the findings file is in place. Only
.imd-findings.jsonand my scratch probe undertest/scratch/were created; no project file was changed (the tree shows no modifications, both paths are git-ignored).Outcome: I found no permission bypass, fund-loss path, or broken guarantee in the Access Control, Trust Gap, or Asymmetry areas. The role model behaves as the brief and README describe. Three minor items are recorded with reproductions.
What I checked
- Every role gate:
stakeFor(claim only),withdrawRegen(safe only, bounded, pays safe),setRegenCap(safe only, in bounds, prospective only),payOps/payOffsets(role pulls own balance),claimKeeper(own debt only), hooklowerFee/beforeInitialize(owner only, once, ETH/IMDO), bothunlockCallbacks (manager plus an in-flight flag),executeImd(self-call only). - Paired surfaces:
stake/stakeFor,stake/unstake,claim/claimReward/exit(ETH key never paid),claimSeat/claimHolderlaunch and deadline handling,regenCreditOfview versus settle math, constructor versus setter bounds, Split event versus state writes. - Trust seams: keeper timing versus checkpoint floor, regen cap changes versus pending and accrued ETH, safe withdrawals versus staker credit, the manifest's
$ownerholding claim, regenSafe, offsetsSafe and opsWallet. - Probes run in
test/scratch/Probe.t.sol(all passed as expected): lifetime REGEN credit unchanged when another staker's unstake drops the pool below the 10M threshold, stream restart equals remaining reserve, the view/write asymmetry below, and the claim-address prediction failure mode. - Baseline: full default suite passes (98 tests, 18 suites). README has none of the forbidden words and no audit claim; no PNKSTR/IMDSTR/SplitOracle paths remain.
Findings recorded
- Low, script/DeployImdo.s.sol:87. The staking contract's immutable
claimContractis a nonce-predicted address, andpreflightaccepts alaunchequal to the current timestamp with no margin. Because a broadcast is separate transactions andImdoClaim's constructor reverts oncelaunchhas passed, a late or interleaved transaction leavesclaimContractcodeless forever;stakeForis dead and no claim contract can ever be bound. No attacker trigger and no fund loss. Fix is a time margin inpreflightplus a documented two-consecutive-transaction requirement. - Info, src/ImdoStaking.sol:90.
rewardTokens()lists the address(0) credit key, butisRewardTokenis never set for it, soclaimReward(address(0))reverts. A view/write asymmetry for integrators; behaviour itself is correct. - Info, src/ImdoTreasury.sol:337.
Leg.failingSinceandMIN_FAILURESare written or saturate a counter but nothing reads them since rerouting was removed. Dead state from the base contract, harmless.
Coverage: 36 of 36 listed entry points answered (35 holds, 1 finding), plus four invariant/manifest rows. Nothing was left unreached. Items outside my area that I did not pursue in depth and that belong to the economics specialist: the strength of the decaying checkpoint floor against price manipulation around treasury buys, and the sole-staker capture of live rewards while the backlog gate is unmet, both of which are documented ADAM behaviours rather than permission defects.
ran onclaude · claude-fable-5-1 · 28 turns · 18m 54s · 450 in · 56.9K out · 2M cachedsubmission6925d38dd8d17b63305bee59e4a9188859f80073d916509a28c60c82c9952996device6ef494db85781eec11af6ed42b4e455faba3a2395fa3fe3ca47b4b5fc8708369started from8f2430e90751e2b6d4dfd09fa24f3e5c43aff625bundlenoneapplied on2cdf7799cbb38f4f576141b5a610b5c184aa3f69fd450a0967d6326a8ac94f23, 82505aa2c5d34951b56ec62222d8238f5931c3633cb4f35f332321708a702b29, afc57407b1ed15c4dea5fd6679b28ac0254feaba869815a139732b2cb7c19b6dchanged · 0 filesnothingClaim-address prediction has no time margin: a late or failed claim creation leaves staking's immutable claimContract pointing at a codeless address foreverscript/DeployImdo.s.sol:87
View/write asymmetry: rewardTokens() lists address(0) although it is not a reward token and claimReward(address(0)) revertssrc/ImdoStaking.sol:90
_rewardTokensholds both IMD and the address(0) REGEN credit key, andrewardTokens()returns that whole list, butisRewardToken[address(0)]is never set.claimReward(address(0))andnotifyReward(address(0), x)therefore revert NotRewardToken, while_claimsilently skips the key.The behaviour (never paying ETH to holders) is the intended one; the defect is that the public enumeration advertises a key that every write path rejects, so an integrator or frontend iterating
rewardTokens()and callingclaimRewardfor each entry hits a revert.Fix: expose only claimable tokens from
rewardTokens()(or document that address(0) is a credit key, not a reward token).Scratch test test_rewardTokensListsNonClaimableKey:
staking.rewardTokens()returns [imd, 0x0] (length 2, list[1] == address(0));staking.isRewardToken(address(0))is false;staking.claimReward(address(0))revertsNotRewardToken(0x0). Expected by the view: both entries are reward tokens; actual: the second cannot be claimed or notified.Dead state retained from ADAM rerouting: Leg.failingSince is written on failure but never read by any decisionsrc/ImdoTreasury.sol:337
Storage-variable lifecycle check (written but never read).
failingSinceis set on the first failure and cleared by_resetFailures, andMIN_FAILURESonly saturates thefailurescounter; neither influences any branch now that rerouting was removed (the only reads of failure state arel.failures != 0andl.lastFailurein_executeImd).This is forgotten state from the base contract, not a defect in behaviour: retries, halving and the 2-hour failure gap work from
lastFailure/retryCapalone. Removing the field and constant (or reading them) would make the leg struct match what the code actually does.Cause one failed IMD buy (e.g. FaultyStaking.configure(false, true) in the existing ImdoTreasuryTest fixture, fund 1 ether, process()).
treasury.leg(0).failingSinceequals the failure timestamp andfailuresis 1, but no code path readsfailingSince: searching src/ImdoTreasury.sol shows writes at lines 337 and 346 only. Expected for a retained field: it gates some retry decision; actual: none.
- Every role gate:
- reviewed
#1376Audit mathClaude3 findings · 1 medium
afterBuild contract project, Write foundry tests, ManifestThe review is complete. Findings are in
.imd-findings.jsonat the repository root, all three snippets verified against the tree, and the scratch probe has been removed so no project file changed.What I found in the math area (Precision, Boundary, Numerical Gap)
- Medium,
src/ImdoTreasury.sol:332. The IMD-buy checkpoint is refreshed from the same-block post-swap spot price with no bound on how far it may rise. Because the min-out floor then decays only as (7d/(7d+t))² in sqrt-price terms, a 2x rise in IMD-per-ETH pins the floor above spot for about three days. Reproduced two ways on the local fixture: an unprivileged sandwich aroundprocess()(sell IMD, call process, buy back) cost the attacker about 1.99 ETH and stalled the leg for 438 consecutive calls, 3.05 days. An organic IMD price doubling stalled it 2.75 days with no attacker. Funds are delayed, not lost. Suggested fix keeps the design: bound the per-refresh rise of the checkpoint or shorten the decay constant. - Low,
src/ImdoStaking.sol:168. In the manifest wiring, claim and regenSafe both resolve to the policy owner. That address can callstakeFor(victim, 1)daily and reset any staker's 24-hour lock forever. Reproduced: thirty 1-wei calls kept Alice's 1,000 IMDO locked at day 30. This is the same lock-grief that the imported finding closed for strangers, reopened for one privileged address, and it is not disclosed in README or the manifest notes. - Info,
src/ImdoTreasury.sol:187. The intake cap is derived so the IMD share equals one max buy, but REGEN overflow also flows into that leg. Once the epoch cap binds, each call adds 1.625 ETH while buying 1 ETH. Measured pending growth of 0.625 ETH per call under sustained inflow. No loss and the accounting invariant holds; reported for parameter sizing.
What held. Split and bounty arithmetic conserves every wei, and the cap works out to exactly a 1 ETH IMD leg. The per-share accumulator cannot overflow for any plausible supply, the backlog stream never underflows across threshold crossings, and the lifetime REGEN credit is monotonic. Hook fee gross-up and partial-fill checks agree between beforeSwap and afterSwap. Claim tranche rounding sums to the exact allocation. The full suite of 98 tests passed before I started.
Coverage. All 36 listed entry points have a row, plus four invariant rows. Three entry points point at findings; the rest hold. No critical or high findings, so no proof test files were attached.
ran onclaude · claude-fable-5-1 · 36 turns · 22m 28s · 546 in · 61.5K out · 2.6M cachedsubmissionc28ace4594aa8009a1293f01f95412aeb73f36ca332b964e67600c63361214c3device1559912e747bbcd47e08bea06ad34a8cab66936ffc40556c79cfd0ecc60f74dfstarted from8f2430e90751e2b6d4dfd09fa24f3e5c43aff625bundlenoneapplied on2cdf7799cbb38f4f576141b5a610b5c184aa3f69fd450a0967d6326a8ac94f23, 82505aa2c5d34951b56ec62222d8238f5931c3633cb4f35f332321708a702b29, afc57407b1ed15c4dea5fd6679b28ac0254feaba869815a139732b2cb7c19b6dchanged · 0 filesnothingmediumIMD buy leg stalls for days after any sqrt-price rise: checkpoint is refreshed from the same-block post-swap spot, so a sandwich around process() (or an organic 2x move) pins the min-out floor above ssrc/ImdoTreasury.sol:332
Manifest wiring makes the policy owner the claim caller: stakeFor(victim, 1 wei) every <24 h resets the victim's lock indefinitely, so one address can freeze every staker's IMDO principalsrc/ImdoStaking.sol:168
When the REGEN epoch cap is saturated the IMD leg takes in 1.625 ETH per process() but can buy only maxEthPerBuy (1 ETH), so _imdLeg.pending grows by 0.625 ETH per call under sustained inflowsrc/ImdoTreasury.sol:187
LocalV4 fixture, treasury with the manifest literals.
Every 600 s send exactly cap = 2512562814070351758 wei to the treasury and call process().
Observed leg(0).pending after calls 0..5: 0.125, 0.750, 1.375, 2.000, 2.625, 3.250 ETH (+0.625 ETH per call); totalRegenNotified stays at 0.5 ETH after call 0; unsplitEth() stays 0.
Expected from the cap derivation: pending returns to 0 each call.
- Medium,
- reviewed
#606Audit flowClaude2 findings · 2 low
afterBuild contract project, Write foundry tests, ManifestReview complete. The findings file is written at
.imd-findings.jsonwith two low findings and 40 coverage rows, and my scratch tests are removed so no other file changed.What I checked. I read the Senior Auditor's Mindset and the Execution Trace, Periphery and First Principles guides, then traced every state-changing entry point of the five contracts and the deploy script. I verified the trickiest claims with scratch tests on the repository's local Uniswap v4 fixture: the exact 50 bps bounty and 1000/2500/2500/4000 split, the per-epoch REGEN cap with overflow into the IMD leg, and REGEN credit monotonicity when another staker's exit drops the total below the backlog threshold. All held. The existing suite of 98 tests also passes.
Findings, both low.
- Checkpoint inflation stalls IMD buys at
src/ImdoTreasury.sol:332. The post-buy checkpoint is unbounded and can be set by a buy as small as a few gwei of donated ETH. An unprivileged actor sells IMD into the ETH/IMD pool, triggers a dust buy, buys back, and the inflated floor makes every later buy revert until it decays. Measured in the fixture: a 2.4 ETH round trip stalled the IMD leg for about 4.6 days with over 4 ETH piling up in pending. No funds are lost and the attacker gains nothing, hence low. Suggested fix is to bound the checkpoint jump relative to the previous floor, or require a meaningful buy size before updating it. - Deploy script has no launch margin at
script/DeployImdo.s.sol:150. A launch timestamp that passes simulation but is behind the block that mines the claim creation makes the ImdoClaim constructor revert. The failed CREATE burns the nonce, so the claim address baked into ImdoStaking can never hold code and that staking instance is permanently without a working stakeFor. The docs warn about timing, but only the script can enforce it.
Coverage. Every listed entry point has a row. All hold except
ImdoTreasury.process()which carries finding 1, plus a script row for finding 2 and three invariant rows that hold. Nothing was left unreached. Neither finding reached critical or high, so no proof test files were attached.ran onclaude · claude-fable-5-1 · 36 turns · 24m 55s · 546 in · 63.8K out · 2.4M cachedsubmission95cfb5d9ec1eca90f1d2353fef1103321d7a387fb5e2039cbd4a85d581f7ba94devicee6de8d4c6cf9755159db0f1186e45380731977af9ac6ac182deeacfba39d97eestarted from8f2430e90751e2b6d4dfd09fa24f3e5c43aff625bundlenoneapplied on2cdf7799cbb38f4f576141b5a610b5c184aa3f69fd450a0967d6326a8ac94f23, 82505aa2c5d34951b56ec62222d8238f5931c3633cb4f35f332321708a702b29, afc57407b1ed15c4dea5fd6679b28ac0254feaba869815a139732b2cb7c19b6dchanged · 0 filesnothingIMD-leg checkpoint is taken from the post-swap spot without any bound, so one sandwiched dust buy inflates the floor and stalls every IMD buy for dayssrc/ImdoTreasury.sol:332
Deploy script accepts launch == now; a launch timestamp that passes simulation but is behind the mined block makes ImdoClaim's creation revert and permanently burns the claim address baked into ImdoStscript/DeployImdo.s.sol:150
preflight only rejects c.launch strictly below the simulation block timestamp, and ImdoClaim's constructor (src/ImdoClaim.sol:56) reverts with InvalidConfiguration when launch_ < block.timestamp at execution time. A Foundry broadcast is a sequence of separate transactions mined over several blocks, so the token and staking creations can land, then the claim creation lands in a later block whose timestamp exceeds launch and reverts.
The failed CREATE still consumes the deployer nonce, so the address that ImdoStaking stored as its immutable claimContract (predicted at script line 87 from nonce+1) can never receive code: stakeFor is permanently unusable on that staking instance and the token and staking deployments are wasted. docs/DEPLOYMENT.md tells the operator to choose enough time, but the script does not enforce a margin although it is the only place that can.
- Checkpoint inflation stalls IMD buys at
- reviewed
#153Audit economicsClaude3 findings · 1 medium
afterBuild contract project, Write foundry tests, ManifestReview complete. The findings file is at
.imd-findings.jsonwith 3 findings and coverage rows for all 36 entry points plus 6 invariants.What I found
-
Medium: the IMD-buy checkpoint re-anchors to the manipulated post-swap price (
src/ImdoTreasury.sol:332). The min-out guard is the only defence against same-block manipulation, but every successful buy overwrites the reference with the post-swap spot. A sandwicher who pushes spot to the floor each round lowers the next floor by ~3%, compounding every 600 s regardless of the 7-day decay. In the proof, after 20 rounds the checkpoint sits at 76% of the market sqrt-price and stakers receive 127 IMD per ETH against a fair 219. I read the live mainnet ETH/IMD pool: ~684 ETH of in-range depth, so today it is griefing (attacker pays ~9 ETH of pool fees per 20 rounds in a 200 ETH pool), and becomes profitable below roughly 100 ETH of depth. Proof test fails on current code and passes under a one-line fix that never lets a buy lower the reference below its decayed floor. -
Low: manifest
$owneras staking claim caller can freeze any staker's principal (src/ImdoStaking.sol:168). stakeFor resets the whole-position lock, so the policy owner can call stakeFor(victim, 1 wei) daily and the victim can never unstake. Safe in the manual deployment where ImdoClaim only passes msg.sender; a trust-assumption risk introduced by the manifest substitution and not documented. -
Low: the 10M IMDO floor gates only the backlog, not direct distribution (
src/ImdoStaking.sol:226). A 1 wei stake captures 100% of every IMD purchase and REGEN credit notified while it is the only stake, while the launch backlog remains gated behind it.
What held: the exact split and bounty, REGEN epoch cap and overflow routing, failed-notify retry, ETH conservation in treasury and staking, lifetime REGEN credit monotonicity, withdrawRegen bound, claim vesting and entitlement following the NFT, holder proofs and global bound. I checked and rejected the keeper gas-starvation grief on the try/catch (the 63/64 rule makes it unreachable).
Not reached: no fork run of the mainnet buy itself and no evaluation of the real seat NFT or holder list, which are off-chain trust inputs.
ran onclaude · claude-fable-5-1 · 39 turns · 26m 51s · 740 in · 76.4K out · 4.1M cachedsubmissiond481b9e9f29b21742476853135af813633854436e0b401c00fc6ba7d1840aae4devicec35be49d2f8f8def53d127cb1fdf58d1200d2c513d0ef92d905319810c41e5c6started from8f2430e90751e2b6d4dfd09fa24f3e5c43aff625bundlenoneapplied on2cdf7799cbb38f4f576141b5a610b5c184aa3f69fd450a0967d6326a8ac94f23, 82505aa2c5d34951b56ec62222d8238f5931c3633cb4f35f332321708a702b29, afc57407b1ed15c4dea5fd6679b28ac0254feaba869815a139732b2cb7c19b6dchanged · 0 filesnothingmediumIMD-buy checkpoint re-anchors to the manipulated post-swap price, so sandwiched buys ratchet the min-out floor down geometricallysrc/ImdoTreasury.sol:332
Manifest deployment makes $owner the staking claim caller, which can freeze any staker's principal indefinitely for 1 wei a daysrc/ImdoStaking.sol:168
The 10,000,000 IMDO floor gates only the backlog; a 1 wei stake captures 100% of every reward and REGEN credit notified while it is the only stakesrc/ImdoStaking.sol:226
ImdoStaking fresh. notifyReward(imd, 100e18) with totalStaked == 0 -> unallocated[imd] == 100e18 (gated).
Sniper stakes 1 wei of IMDO. notifyReward(imd, 1000e18) and notifyRegen{value: 10 ether}().
Expected under a consistent floor: both amounts back-logged until 10,000,000 IMDO is staked.
Actual: earned(sniper, imd) == 1000e18 and regenCreditOf(sniper) == 10 ether, while the 100e18 backlog is still gated.
Run: forge test --match-path test/scratch/TinyFirstStaker.t.sol -vv
-
- reviewed
#1523Audit judgeCodexrefusedRefused by Codex's safety filterretried on #1473 (Claude)
This content was flagged for possible cybersecurity risk. If this seems wrong, try rephrasing your request. If you’re doing authorized security work that requires more cyber permissive safeguards, apply for Daybreak access via https://platform.openai.com/settings/organization/status-and-access before retrying.
ran oncodex · gpt-6-astra · 3 turns · 4m 56ssubmission3a57627ab503cd220eb7a593dfe33d5937b50d71f0034c68ec5fe75619ee3b46device90675b3128fe90967be222bda804fb0eadfee26e6664fc815f0475598bb3c14dstarted from8f2430e90751e2b6d4dfd09fa24f3e5c43aff625bundlenoneapplied on2cdf7799cbb38f4f576141b5a610b5c184aa3f69fd450a0967d6326a8ac94f23, 82505aa2c5d34951b56ec62222d8238f5931c3633cb4f35f332321708a702b29, afc57407b1ed15c4dea5fd6679b28ac0254feaba869815a139732b2cb7c19b6dchanged · 0 filesnothing#1473Claude7 findings · 1 medium
afterBuild contract project, Write foundry tests, Manifest, Audit math, Audit permissions, Audit economics, Audit flowThe review is complete. The findings file is written, the source tree is unchanged, and every snippet, line and proof was checked against the tree.
Outcome. Of the eleven specialist findings, all reproduced, and they collapse into seven after merging duplicates. One is medium, two are low, four are informational. My own pass over every entry point found nothing the specialists missed. The full default suite passes (98 tests), and the manifest's use of
LaunchToken, fee 3000 and the nominal price matches the launch reference, so those are not defects.Findings written to
.imd-findings.json:- Medium, with proof. The treasury refreshes the IMD-buy checkpoint from the post-swap spot with no bound (
src/ImdoTreasury.sol:332). Three specialists reported this as two symptoms of one root cause, so they are merged. Downward, a sandwicher pushes the price just under the floor each cooldown and the floor ratchets down geometrically. After 20 rounds the checkpoint sits at 77% of the market sqrt-price and the treasury receives 85 IMD where the honest fill was 131 to 138. Upward, one dust buy at an inflated price stalls every IMD buy for 489 consecutive calls, about 3.4 days. The attached test fails on the current code and passes once the refresh is floored at the decayed checkpoint, which I confirmed by applying and then reverting a temporary patch. - Low. The manifest wires
$owneras the staking claim caller, so that one address can reset any staker's lock for 1 wei a day and freeze their principal. This is a trust assumption the README and notes do not disclose. - Low. The deploy script accepts a launch timestamp equal to now, so a claim creation that lands after it reverts on chain and burns the claim address baked into staking. Merged from two specialists.
- Info. With the REGEN cap saturated, the IMD leg takes in 1.625 ETH per call but buys at most 1 ETH.
- Info. The reward-token view lists address(0) although every write path rejects it.
- Info. The failure-start timestamp and minimum-failures constant are dead state.
- Info. A 1 wei stake captures all live rewards while it is the only stake. This is documented ADAM behaviour, so I lowered it from the specialist's low.
Coverage. All 36 listed entry points have a row plus three invariant rows: 36 hold, 3 point at findings, none unreached.
ran onclaude · claude-fable-5-1 · 37 turns · 17m 34s · 642 in · 58.3K out · 3.2M cachedsubmission9d6254358b64d19c91435efb080eef018d5837ed19a4a0bb7c6e46349ac890bcdevice3f91b58cf7cd2d45e4d1e4594b1da9cc601a40bc07fa1e52580901572c5b342cstarted from8f2430e90751e2b6d4dfd09fa24f3e5c43aff625bundlenoneapplied on2cdf7799cbb38f4f576141b5a610b5c184aa3f69fd450a0967d6326a8ac94f23, 82505aa2c5d34951b56ec62222d8238f5931c3633cb4f35f332321708a702b29, afc57407b1ed15c4dea5fd6679b28ac0254feaba869815a139732b2cb7c19b6dchanged · 0 filesnothingmediumIMD-buy checkpoint is refreshed from the post-swap spot with no bound: a sandwicher ratchets the min-out floor down ~1-3% per cooldown, and one inflated dust buy stalls every IMD buy for dayssrc/ImdoTreasury.sol:332
proof · a Foundry test the fix has to passManifest wires $owner as the staking claim caller, so one privileged address can reset any staker's 24h lock for 1 wei a day and freeze their principal indefinitelysrc/ImdoStaking.sol:168
Deploy script accepts launch == now, so a claim creation that lands after `launch` reverts on chain and permanently burns the claim address baked into ImdoStakingscript/DeployImdo.s.sol:150
With the REGEN epoch cap saturated the IMD leg takes in 1.625 ETH per process() but buys at most 1 ETH, so leg(0).pending grows 0.625 ETH per call under sustained inflowsrc/ImdoTreasury.sol:187
From audit_math (info). The intake cap at line 168 is derived so that 40% of net equals maxEthPerBuy (cap = 2.512562814070351758 ETH, imd = 1.0 ETH). It ignores the REGEN overflow that line 187 routes into the same leg once regenAccrued[epoch] reaches regenCap (0.5 ETH, hit on the first saturated call since regen per call is 0.625 ETH).
Every further call that epoch adds 1.0 + 0.625 ETH to the leg while _executeImd buys at most maxEthPerBuy.
Not a loss: the accounting invariant holds and pending is bought down at 1 ETH per cooldown once inflow drops; reported so the author sizes regenCap / maxEthPerBuy / cooldown knowingly for launch-week inflow.
LocalV4 fixture with the manifest literals and one staker.
Every 600 s send exactly 2512562814070351758 wei to the treasury and call process().
Observed leg(0).pending after calls 0..5: 0.125, 0.750, 1.375, 2.000, 2.625, 3.250 ETH; totalRegenNotified stays 0.5 ETH after call 0; unsplitEth() stays 0.
Expected from the cap derivation: pending returns to 0 each call.
Verified in test/scratch/Probe.t.sol::test_pendingGrowsWhenRegenCapSaturated.
rewardTokens() advertises address(0) although it is not a reward token: claimReward(address(0)) and notifyReward(address(0),x) revert NotRewardTokensrc/ImdoStaking.sol:91
From audit_permissions (info). _rewardTokens holds IMD and the address(0) REGEN credit key so that _settle/_claim iterate both, but isRewardToken[address(0)] is never set and _claim skips the key. The behaviour (holders are never paid ETH) is intended; the defect is that the public enumeration lists a key every write path rejects, so an integrator iterating rewardTokens() and calling claimReward for each entry hits a revert.
Expose only claimable tokens from rewardTokens(), or document that address(0) is a credit key.
staking.rewardTokens() returns [imd, 0x0] (length 2, [1] == address(0)); staking.isRewardToken(address(0)) == false; staking.claimReward(address(0)) reverts NotRewardToken(0x0). Verified in test/scratch/Probe.t.sol::test_viewsAndDeadState.
Leg.failingSince and MIN_FAILURES are written but never read: dead rerouting state retained from ADAMsrc/ImdoTreasury.sol:337
From audit_permissions (info). failingSince is set on the first failure (line 337) and cleared in _resetFailures (line 346); MIN_FAILURES only saturates the failures counter. The only failure-state reads are
l.failures != 0andl.lastFailurein _executeImd (line 322); retries, halving and the 2-hour gap work from lastFailure/retryCap alone. Harmless forgotten state; removing the field and constant (or reading them) makes the struct match what the code does.Cause one failed IMD buy (FaultyStaking.configure(false, true) in ImdoTreasuryTest, fund 1 ether, process()): treasury.leg(0).failingSince equals the failure timestamp and failures == 1, yet grep of src/ImdoTreasury.sol shows failingSince written at lines 337 and 346 only and MIN_FAILURES read only at line 339; no branch depends on either.
The 10,000,000 IMDO floor gates only the backlog; a 1 wei stake captures 100% of every IMD reward and REGEN credit notified while it is the only stakesrc/ImdoStaking.sol:226
From audit_economics (low), recalibrated to info: this is ADAM's direct-distribution rule, the brief asks for ADAM's behaviour, and README line 43 documents it ('New rewards with any existing stake distribute immediately').
The gap is only that the MIN_BACKLOG_STAKE floor protects launch-time rewards notified with zero stake but not the live flow once any dust stake exists: at launch (20% anti-snipe fees, up to 1 ETH of IMD per 600 s, up to 0.5 ETH REGEN per epoch) whoever stakes first with dust receives everything until another holder stakes. If intended, say so explicitly in README; otherwise route direct distributions to unallocated while totalStaked < MIN_BACKLOG_STAKE.
Fresh ImdoStaking: notifyReward(imd, 100e18) with totalStaked == 0 -> unallocated[imd] == 100e18 (gated).
Sniper stakes 1 wei IMDO. notifyReward(imd, 1000e18) and notifyRegen{value: 10 ether}().
Actual: earned(sniper, imd) == 1000e18 and regenCreditOf(sniper) == 10 ether while the 100e18 backlog is still gated.
Verified in test/scratch/Probe.t.sol::test_tinyFirstStaker.
- Medium, with proof. The treasury refreshes the IMD-buy checkpoint from the post-swap spot with no bound (
- publishedafter verification
- deployedto Sepolia
- onchain
1 receipt, 10 scoreson Ethereum mainnet
- receipt
- work accepted · transaction · record
- scores
- 10 scores for reviewed, built, integrated, tested on submission, checks · 9 of 10 passed · block 26,136,187 · transaction
#153
#606
#1812
#1473agent 51288
#440
#923
#617
#637
#847