{"assessments":[],"deployments":[],"fuzz":[],"identity":{"adapter":"0xde152afb7db5373f34876e1499fbd893a82dd336","chainId":1,"collection":"0x0000ec93127baa929e58e97dd0095a2bfb38ec1d","registry":"0x8004a169fb4a3325136eb29fa0ceb6d2e539a432"},"interpretation":"Records acceptance and evidence. Neither completion nor an AI assessment establishes correctness, safety, or independent review.","jobId":"26cf0d3a-2bd3-4602-8cb8-58ba9beb5c8a","kind":"audit","nodes":[{"acceptedSubmissionHash":"c59a5dd4d4fa14c6da9fea803efe888d9ec628d8714ebd55e9729f0d6c54d3b7","dependsOn":[],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_economics","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"ce6c0b4c904ec4f11e336835cb5c1b846a12f50bf1eed80d2ac58a9db5a88320","dependsOn":[],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_flow","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"2da711c16a8b1432d5d6a2a1dc5ef02388313272f4b4ce72c11f5d50c81c7b12","dependsOn":["audit_math","audit_permissions","audit_economics","audit_flow"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"3014f1ea5961918ca059453a484bf4c8bcbbfc2248dbe31d94ac7c5cdf8f50bd","skillId":"audit-judge","tools":[]},"key":"audit_judge","kind":"code","role":"review","skillHash":"3014f1ea5961918ca059453a484bf4c8bcbbfc2248dbe31d94ac7c5cdf8f50bd","skillId":"audit-judge","state":"accepted"},{"acceptedSubmissionHash":"81769c4c8b5d3e40909d2ca9e85f0b1499bf4c3f886131c71c6a2496a38dc8d3","dependsOn":[],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_math","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"cff631e9d03d4ba2645dd8b3499e5e44971402c2c732b4a1e7c295bd39b27d35","dependsOn":[],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_permissions","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"}],"objective":"Audit the SOVRN.ONE / SVO launch contracts in this repository (Solidity 0.8.26, Foundry, Uniswap v4 vendored in lib/). Scope: src/SovrnHook.sol, src/LifeForceVault.sol, src/SovrnToken.sol, src/HookFlags.sol, src/Interfaces.sol, script/PrepareLaunch.s.sol, launch.json, README.md. Tests in test/ (167 tests, run in both currency orders, plus test/Fork4663.t.sol which runs against the real chain when FORK_4663_RPC is set) are evidence to check, not the object of the audit. Intended deployment: Robinhood Chain (chain id 4663), a Uniswap v4 pool of {IMD, SVO} where IMD is the ERC-20 at 0x5F7Bb59365ce557C26dbcAa4EE9d39A4b95B7127 and SVO is a plain fixed-supply token; every trading fee is paid in IMD to an immutable LifeForceVault that only accounts for it; a fixed Safe (0xEb57c52272B90F989C41B739e2ccc5f00bF7697C) withdraws by hand. Write nothing to the repository; deliver report.md. Tone: factual and plain; no claims of safety beyond the evidence; do not call the contracts audited or secure; no investment language.\n\nCONTEXT. This code was adapted from an accepted ETH-paired version (the first commits of the branch, where the fee was native ETH): the fee currency became IMD, the hook now handles IMD as either currency0 or currency1 (decided by address order, `imdIsCurrency0`), and the vault now derives its reserves from IMD.balanceOf. Review that diff with particular care: the ETH-to-IMD generalisation is where new defects are most likely.\n\nHARD QUESTIONS (answer each with a verdict and evidence, and a reproducible Foundry test where possible):\n1. Currency order. For both orders and for all four exact-input/exact-output modes, is `buy = (zeroForOne == imdIsCurrency0)` and `specifiedIMD = (buy == (amountSpecified < 0))` correct, are the IMD leg (amount0 vs amount1) and every sign in beforeSwap/afterSwap and their return deltas right, and does the fee always equal the stated percentage of the actual IMD leg? Can any price limit, tiny amount or rounding produce a fee that differs from the spec, a revert on a valid swap, or a fee larger than the amount?\n2. Quote mechanism. The hook measures the real IMD delta with a self-call that always reverts, then requires the real swap to match (QuoteMismatch). Can that be broken or griefed (reentrancy, the busy flag, transient state, protocol fees, an LP-fee override, a hook-less path, concurrent unlocks)? Can the quote leave state behind?\n3. ERC-20 fee path. Fees are taken with PoolManager.take(IMD, vault, fee), or minted as ERC-6909 claims (id uint160(IMD)) when the manager holds less IMD than the fee, then redeemed by the permissionless redeemFees(). Is the manager-balance check right, can claims be stranded or double-spent, can redeemFees be reentered, and what exactly happens if IMD reverts, returns false, takes a transfer fee, or calls back (ERC-777 style) during take or transfer?\n4. Vault accounting. The vault has no receive hook for an ERC-20, so _reserves() derives reserves from IMD.balanceOf with a checkpoint model, floor(x*3000/10000) to buyback, a clamp so reserves never exceed the real balance (shortfall reduces buyback first), sync(), and Safe-only withdrawals paid with a low-level call. Can the Safe withdraw more than it should, can anyone grief or steal, can reserves ever exceed the balance or underflow, are rounding and dust handled, is nonReentrant correct, and is the low-level transfer return handling safe for non-standard ERC-20s? What does a malicious or upgraded IMD change?\n5. Initialisation and addresses. beforeInitialize binds one pool: factory-only, exact currencies in address order, fee 12500, hooks == this, tickSpacing > 0. Is that complete? Hook flags 8396: does PrepareLaunch mine a valid address, can a hook with the wrong flags or a pre-initialised address slip through, does the constructor guard (block.chainid == 4663, IMD has code, token != IMD) hold, and is anything wrong with deploying the vault inside the hook constructor?\n6. Opening-price and launch risks. What can an attacker do between pool initialisation and liquidity seeding (empty pool zero-delta swap, price manipulation, sandwiching the first-hour decaying buy fee, block.timestamp use)? Is anything in the README wrong or missing about this?\n7. Trust and operational assumptions. IMD has an owner and unknown transfer rules; the pool manager has a protocol-fee controller; one Safe has custody of every withdrawal. State precisely what each can and cannot do to funds and trading, and whether the code or README understate any of it. In particular: what happens to trading if IMD blocks transfers to the vault?\n8. Token. Confirm SovrnToken is plain (no owner, mint, tax, pause, blacklist), name() is exactly \"SOVRN.ONE\" and symbol() exactly \"SVO\", supply 10^27, and burn() on the vault sends only to DEAD and only SVO.\n9. Tests and docs. Does the test suite genuinely cover the risks above in both orders (incl. mocks that misbehave), what is missing, and does every claim in README.md and launch.json match the code exactly (numbers, addresses, privileges, wording rules)?\n\nMETHOD: use the Pashov methodology and specialties. Reproduce every finding against the code; discard unreproducible claims. Rate each finding by severity and likelihood, give a concrete fix, and separate real defects from documented design trade-offs. Do not claim this review substitutes for an independent human audit.","parentJobId":null,"planHash":"26e7e9f3c117c946099e3963064cf79b47fda2dbec5f222ad39c0d9c3b638f85","previousHash":"0000000000000000000000000000000000000000000000000000000000000000","projectId":"26cf0d3a-2bd3-4602-8cb8-58ba9beb5c8a","publication":{"commit":null,"deliveredAt":null,"repoUrl":null},"receiptIdentity":{"adapter":"0xde152afb7db5373f34876e1499fbd893a82dd336","chainId":1,"collection":"0x0000ec93127baa929e58e97dd0095a2bfb38ec1d","registry":"0x8004a169fb4a3325136eb29fa0ceb6d2e539a432"},"registry":"0xb6d0a187b050fa5bb0b87033a203f37becf4a775","research":[],"schema":"identitymd-work-v1","signals":[{"agentId":"51450","feedbackHash":"086ec0cbe93681eb9fbd585b0b9adac6bd79edea500d2630154af7a0acca27bc","nodeKey":"audit_economics","submissionHash":"c59a5dd4d4fa14c6da9fea803efe888d9ec628d8714ebd55e9729f0d6c54d3b7","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51507","feedbackHash":"89aac87dde46b2d39e3ec4efd7348d1024a7e0da2722e3a76b3166d76a4c6282","nodeKey":"audit_flow","submissionHash":"ce6c0b4c904ec4f11e336835cb5c1b846a12f50bf1eed80d2ac58a9db5a88320","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51039","feedbackHash":"451ba30125948cd567ad8aa9641b9369366bb91aa659820ada343d742efd1104","nodeKey":"audit_judge","submissionHash":"2da711c16a8b1432d5d6a2a1dc5ef02388313272f4b4ce72c11f5d50c81c7b12","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51163","feedbackHash":"55b844b2775589e1bb4b5651f5b1c706f7291f18096568468c3c98882993b4a9","nodeKey":"audit_math","submissionHash":"81769c4c8b5d3e40909d2ca9e85f0b1499bf4c3f886131c71c6a2496a38dc8d3","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51032","feedbackHash":"126c4db6c5287e129c13cd8552a2f5e316d8862ff9c1cd2346afe16f7f75cc0e","nodeKey":"audit_permissions","submissionHash":"cff631e9d03d4ba2645dd8b3499e5e44971402c2c732b4a1e7c295bd39b27d35","tag1":"review:submission","tag2":"acceptance-v2","value":1}],"site":null,"snapshotHash":"8dff7ca5b20b13adc52c999f660c94dc9dfce66eee48fd350a47ffd9c5a8632d","state":"completed","submissions":[{"artifacts":[],"attempt":1,"bundleHash":null,"device":"9ab27edcfd62be02","findings":[{"citation":"resolved","description":"_reserves() clamps the views to the real IMD balance without touching storage, so a transient shortfall (IMD leaving the vault by a route the vault does not control: a seizure, a negative rebase, a transfer fee on an inbound transfer that is later compensated) is reversible as long as nothing is checkpointed. But sync() (permissionless) and both withdraw functions (LifeForceVault.sol:77 and :85) write the clamped values back into storage. Once that happens the buyback checkpoint that was only temporarily unbacked is deleted; any IMD that then returns is treated as new income and split 70/30 on top of the already-full inference checkpoint. The final inference/buyback allocation of identical cash flows therefore depends on whether a third party called sync() while the balance was short, and the drift is one way (buyback never recovers its share). README line 65 documents that a shortfall reduces buyback first but not that the reduction becomes permanent on the next checkpoint, nor that the outcome depends on who calls sync() when. Preconditions are IMD level (the deployed IMD shows no fee or seizure function today, but it has an owner and a blocklist), both reserves pay the same Safe and the README calls the split accounting only, so no value leaves the system: this is a design seam, not a theft path. Merged from four specialist reports (permissions, math, economics, flow). Fix (preserves the design): in sync() and the withdraw functions only raise the checkpoints when balance >= inference + buyback; when the balance is short leave the stored checkpoints as they are and bound the withdrawal by the clamped view (withdrawals still debit the stored reserve and can never exceed the live balance). Alternatively track the shortfall as an explicit deficit that returning IMD repays before any new split. Then state the chosen rule in README 'Vault accounting and callers'.","line":67,"path":"src/LifeForceVault.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\nimport {Test} from \"forge-std/Test.sol\";\nimport {PoolManager} from \"v4-core/src/PoolManager.sol\";\nimport {IPoolManager} from \"v4-core/src/interfaces/IPoolManager.sol\";\nimport {SovrnToken} from \"src/SovrnToken.sol\";\nimport {LifeForceVault} from \"src/LifeForceVault.sol\";\nimport {ERC20} from \"solmate/src/tokens/ERC20.sol\";\n\n/// @dev Plain ERC-20 whose runtime code is placed at IMD's real address (no artifact lookup by file name).\ncontract PlainIMD is ERC20 {\n    constructor() ERC20(\"Identity.md\", \"IMD\", 18) {}\n\n    function mint(address to, uint256 amount) external {\n        _mint(to, amount);\n    }\n}\n\n/// @dev A temporary shortfall (IMD leaves the vault by a route the vault does not control, then the same\n///      amount comes back) must not change the 70/30 split. Today anyone can call sync() during the\n///      shortfall and permanently move the buyback reserve into inference.\ncontract VaultSyncShortfallTest is Test {\n    address constant IMD_ADDR = 0x5F7Bb59365ce557C26dbcAa4EE9d39A4b95B7127;\n    address constant BOB = address(0xB0B);\n    PlainIMD imd;\n    LifeForceVault vault;\n\n    function setUp() public {\n        vm.chainId(4663);\n        PoolManager manager = new PoolManager(address(this));\n        vm.etch(IMD_ADDR, address(new PlainIMD()).code);\n        imd = PlainIMD(IMD_ADDR);\n        imd.mint(address(this), 1e30);\n        SovrnToken token = new SovrnToken();\n        vault = new LifeForceVault(IPoolManager(address(manager)), token, address(0xBEEF));\n    }\n\n    function test_permissionlessSyncDuringShortfallRewritesSplit() public {\n        imd.transfer(address(vault), 100 ether);\n        vault.sync();\n        assertEq(vault.inferenceReserve(), 70 ether);\n        assertEq(vault.buybackReserve(), 30 ether);\n\n        // 30 IMD leave the vault by a route the vault does not control (modelled with a prank).\n        vm.prank(address(vault));\n        imd.transfer(BOB, 30 ether);\n        assertEq(vault.inferenceReserve(), 70 ether);\n        assertEq(vault.buybackReserve(), 0);\n\n        // Anyone checkpoints while the balance is short.\n        vm.prank(BOB);\n        vault.sync();\n\n        // The same 30 IMD come back.\n        vm.prank(BOB);\n        imd.transfer(address(vault), 30 ether);\n\n        // Expected: the original 70 / 30 is restored (the balance equals the original checkpoint again).\n        // Actual: 91 / 9, because the sync() during the shortfall deleted the buyback checkpoint and\n        // the returned 30 IMD is re-split 70/30 on top of inference.\n        assertEq(vault.buybackReserve(), 30 ether, \"buyback reserve permanently reduced by a permissionless sync\");\n        assertEq(vault.inferenceReserve(), 70 ether);\n    }\n}","reproduction":"Plain ERC-20 at 0x5F7Bb59365ce557C26dbcAa4EE9d39A4b95B7127, chain id 4663, LifeForceVault deployed directly. (1) transfer 100e18 IMD to the vault, call sync(): inferenceReserve() 70e18, buybackReserve() 30e18. (2) move 30e18 out of the vault by a route the vault does not control (vm.prank(vault); imd.transfer(BOB, 30e18)): views 70e18 / 0. (3) BOB calls vault.sync(). (4) BOB transfers the same 30e18 back. Expected: balance equals the original checkpoint again, views 70e18 / 30e18 (which is what the views return if step 3 is skipped). Actual: inferenceReserve() 91e18, buybackReserve() 9e18. Reproduced: forge test --match-path test/scratch/VaultSyncShortfall.t.sol fails with '9000000000000000000 != 30000000000000000000'. The two withdraw functions write the same clamped values (line 77 and 85), so a Safe withdrawal during the shortfall has the same one-way effect.","severity":"low","snippet":"        inference = newInference;\n        buyback = newBuyback;","title":"Vault persists a shortfall write-down on sync()/withdraw, so the 70/30 split of IMD that later returns is path dependent and permissionless sync() can move buyback into inference"},{"citation":"resolved","description":"README lines 30-36 and the launch.json notes describe IMD's rules hypothetically. The IMD contract at 0x5F7Bb59365ce557C26dbcAa4EE9d39A4b95B7127 on chain 4663 is a non-proxy (EIP-1967 implementation slot empty) with verifiable owner powers: owner() = 0x047F606fD5b2BaA5f5C6c4aB8958E45CB6B054B7, an address with no code (a single externally owned key); an owner-only setV4Config(address poolManager, address gate, bool) that today is unset (poolManager() = 0x0) and, when set, makes every IMD transfer out of the configured PoolManager revert ('BridgedFP: v4 transfer not approved') unless the gate contract approves it; a blocklist view blocked(address) (false for the PoolManager and the Safe today); a one-way transfersEnabled switch (already true, enableTransfers() reverts 'already enabled'); and a LayerZero OFT bridge (peers(30101) is set), so the owner can re-point a peer and credit IMD on this chain through the bridge path. The README only says that a refused transfer to the vault reverts fee-taking swaps and that 'trading stops'. With the gate on and this pool not approved, every IMD transfer out of the manager reverts: buys (the hook's take of the fee to the vault at SovrnHook.sol:224), sells (the trader's IMD output take), LP removals (the LP's IMD take), and redeemFees(). Liquidity providers therefore cannot exit their IMD either, which the README does not state, and the switch is held by one key. The vault's own withdrawals to the Safe are not manager transfers and keep working unless the vault or the Safe is blocklisted. This is a trust assumption of the agreed design (IMD is an external token), not a code defect. Fix (documentation and launch process): name the gate, the blocklist, the bridge and the single-key owner in README 'IMD assumptions and risks' and in launch.json notes; state that the gate also freezes LP exits; before launch confirm with the IMD owner whether the gate will be enabled and, if so, that this pool's manager transfers are approved; record blocked() for the hook, vault and Safe after deployment.","line":34,"path":"README.md","reproduction":"Live reads against https://rpc.mainnet.chain.robinhood.com (chain id 4663): cast call IMD 'owner()(address)' -> 0x047F606fD5b2BaA5f5C6c4aB8958E45CB6B054B7; cast code of that address -> 0x; cast call IMD 'poolManager()(address)' -> 0x0; 'transfersEnabled()(bool)' -> true; 'blocked(address)(bool)' for the Safe and the real manager -> false; cast call --from <owner> IMD 'setV4Config(address,address,bool)' 0x8366a39CC670B4001A1121B8F6A443A643e40951 0xdEaD true -> succeeds (eth_call), the same from 0x1111...1111 -> OwnableUnauthorizedAccount. Fork reproduction (test/scratch/ForkGate.t.sol, run with FORK_4663_RPC set, passes): deploy token, hook and vault on a fork with the real manager and real IMD, initialize the pool, seed full-range liquidity 1e22, warp past the decay; a 1e18 IMD buy pays its fee into the vault. Then vm.prank(IMD.owner()); IMD.setV4Config(realManager, 0xdead01, true). Afterwards the same buy reverts, an exact-input sell of 1000e18 SVO reverts, and router.liquidity(key, ModifyLiquidityParams(-887220, 887220, -1e21, 0)) reverts (all three are IMD transfers out of the manager), while vault.withdrawInference(1) from the Safe still succeeds. Expected per README: only 'a swap whose fee is taken directly reverts'. Actual: every IMD-moving operation on the manager halts, switchable by one EOA.","severity":"low","snippet":"- If IMD refuses a transfer to the vault (a blacklist, a pause, a transfer hook), a swap whose fee is taken directly reverts. The claims fallback only runs when the manager holds less IMD than the fee, and its redemption would revert for the same reason until the vault can receive IMD. Trading on the hooked pool stops while that lasts.","title":"IMD owner powers understated: the deployed IMD has an owner-only v4 transfer gate (plus blocklist and OFT bridge) controlled by one EOA; switching the gate on halts both swaps and LP IMD withdrawals o"},{"citation":"resolved","description":"withdrawInference and withdrawBuyback (LifeForceVault.sol:76-90) pay only REFUEL_SAFE, and the two reserves together always equal the vault's whole IMD balance, so the Safe's signing policy is the only control over 100% of collected fees. The README and launch.json say the threshold is unverified. It is verifiable on chain: 0xEb57c52272B90F989C41B739e2ccc5f00bF7697C holds a Safe proxy (VERSION 1.5.0) with three owners, all without code, and getThreshold() = 1. One compromised or rogue key among three is enough to drain the vault to the Safe and onward; the contracts have no delay, rate limit or alternative recipient. The single-recipient design is intended, so this is not a code defect. Fix (operational, before launch): raise the threshold to at least 2-of-3, or state in README 'Preparation and operation' and in launch.json that custody is effectively single-key today.","line":97,"path":"README.md","reproduction":"cast call 0xEb57c52272B90F989C41B739e2ccc5f00bF7697C 'getThreshold()(uint256)' --rpc-url https://rpc.mainnet.chain.robinhood.com -> 1; 'getOwners()(address[])' -> [0x217C05f5D1D1E595BBae94534540B803bfC4563B, 0xb1eC9d1C36974d05eb9889eBf8A150b05791E559, 0x7fFA8901be4777D9fD78F9A00D98DFCDBD833671]; 'VERSION()(string)' -> \"1.5.0\" (read on 2026-10-09). With any one of those keys a Safe execTransaction to vault.withdrawInference(inferenceReserve()) and vault.withdrawBuyback(buybackReserve()) moves the entire vault balance to the Safe; the vault checks only msg.sender == REFUEL_SAFE (LifeForceVault.sol:41-44). Expected per README: a threshold confirmed before launch; actual on-chain state: threshold 1.","severity":"low","snippet":"After launch, no setters or setup transactions exist. The Safe operators monitor both reserves and any claim backing, arrange permissionless redemption when needed, withdraw inference funds in IMD (and sell it for the USD that the voice provider requires), and withdraw buyback funds to acquire SVO by hand with appropriate trade limits. They transfer acquired SVO to the vault and anyone calls `burn()`. The contracts enforce the withdrawal destination and reserve bounds; they cannot enforce what the Safe does with withdrawn IMD or schedule its purchases. Operators must confirm control of the specified Safe on chain 4663, including its signing threshold, before launch. The supplied Safe identity is a requester parameter, not independently verified here.","title":"REFUEL_SAFE on chain 4663 is a 1-of-3 Safe today: any single owner key can withdraw every fee IMD (operational trust assumption the README leaves unverified)"},{"citation":"resolved","description":"In the ETH-paired baseline the fee left the manager as native value and native settlement uses msg.value, so a router's settle() never depended on the manager's token balance. With IMD the manager settles ERC-20 input as balanceNow - balanceRecordedBySync. The hook now moves `fee` IMD out of the manager during afterSwap. A router that calls sync(IMD), transfers the input, then swaps and only then settle() (a valid v4 ordering used by pay-first integrations) is credited input - fee by settle() and ends the unlock with an unsettled -fee delta: the swap reverts with CurrencyNotSettled on the hooked pool while the identical call succeeds on a hookless pool. Sells are unaffected (IMD is the output side), and routers that sync immediately before transfer+settle after the swap (Uniswap V4Router, Universal Router) are unaffected. No funds are lost; a class of otherwise valid integrations cannot buy on the hooked pool, and README 'Fees and settlement' does not mention it. Merged from two specialist reports (permissions, economics). Fix: document in README that the hook takes IMD out of the manager during afterSwap, so integrators must settle IMD input after the swap with sync() immediately before settle(), or overpay by the fee; or, if full compatibility is wanted, always mint the fee as ERC-6909 claims and redeem them permissionlessly (a design change that moves every fee through redeemFees()).","line":224,"path":"src/SovrnHook.sol","reproduction":"test/scratch/Judge.t.sol::test_payFirstRouter_hookedVsHookless (passes in both currency orders). Fixture: SystemBase (mock plain ERC-20 at IMD's address, chain id 4663, real vendored PoolManager), hooked pool and an identical hookless pool each seeded with full-range liquidity 1e22, 1 hour after opening. PayFirstRouter.unlockCallback: sync(IMD); IMD.transferFrom(payer, manager, 1e18); swap(key, SwapParams(buy, -1e18, limit)); settle(); take(SVO). Hookless pool: succeeds. Hooked pool, same call: reverts IPoolManager.CurrencyNotSettled (settle credits 1e18 - 0.035e18 while the trader's delta is -1e18). Hooked pool with 1e18 + 0.035e18 pre-paid: succeeds (the router absorbs the fee). Exact-input sell of 1000e18 SVO with the same pay-first ordering on the hooked pool: succeeds. Expected: a v4-valid pay-first buy settles; actual: reverts only on the hooked pool.","severity":"low","snippet":"                poolManager.take(Currency.wrap(IMD), address(vault), fee);","title":"ETH-to-IMD change: taking the fee as IMD inside afterSwap breaks any v4 router that pays the input before swapping (sync -> transfer -> swap -> settle); undocumented integration constraint new to this"},{"citation":"resolved","description":"Line 111 says 'live forks were not run'. Line 109 reports a result that only a fork run can produce ('The real manager's protocol-fee controller assigned this pool a protocol fee of 0 at the time of the rehearsal'), line 107 reports eth_estimateGas measurements taken against Robinhood Chain on 2026-10-08, and the HEAD commit is titled 'Fork rehearsal on real Robinhood Chain'. One of the two statements is wrong, and the task's wording rule is that every README claim matches exactly. Offline the fork suite is skipped (165 passed, 2 skipped), so a reader cannot resolve the contradiction from the tree alone. Fix: replace the sentence on line 111 with an exact statement, e.g. 'Static analyzers and formal verification were not run. test/Fork4663.t.sol was run on 2026-10-08 against https://rpc.mainnet.chain.robinhood.com; it is skipped in a default forge test and must be re-run before launch.'","line":111,"path":"README.md","reproduction":"Read README.md lines 107, 109 and 111 together. Expected: one consistent statement of what was and was not run against a live fork. Actual: line 111 denies any live fork run; lines 107 and 109 report results obtained from a live RPC and a fork. forge test (no FORK_4663_RPC) prints Fork4663ImdLowTest and Fork4663ImdHighTest as skipped; with FORK_4663_RPC set the fork suites and the scratch fork test in this review do run against the real chain, confirming the line-109 kind of result is obtainable.","severity":"low","snippet":"This deliverable includes no deployment transactions. Local tests and source review do not establish live-chain readiness. A separate independent adversarial review and a chain-specific deployment rehearsal remain with the launch process; no external security certification is asserted. Static analyzers, formal verification and live forks were not run. `test/REVIEW.md` and `test/README.md` are historical records of the ETH-paired review.","title":"README states that live forks were not run while the same README reports fork-rehearsal and live-RPC results"},{"citation":"resolved","description":"The hook enables only beforeInitialize, beforeSwap and afterSwap; modifyLiquidity on the hooked pool is unrestricted and carries no hook fee. A participant who wants SVO for IMD can mint an IMD-only position just beyond the current tick and let sellers (who pay 3.5%) push the price through it; burning the position returns SVO acquired with no hook fee while also collecting the 1.25% LP fee. The mirror (SVO-only range on the other side) sells SVO for IMD with no 3.5% fee. This is inherent to a swap-only fee hook and is a design trade-off, not a code error, but it depends on counter-flow and the README's only statement on fee avoidance is about other pools; it says nothing about liquidity on this pool, and the first-hour 50% buy fee is presented as protection of the opening price. Fix: document it in 'Fees and settlement' and in launch.json notes; if the launch policy requires that no path on this pool bypass the buy fee, add beforeAddLiquidity/beforeRemoveLiquidity restrictions (which changes the agreed hook flags and manifest: a scope decision for the requester).","line":40,"path":"README.md","reproduction":"test/scratch/Judge.t.sol::test_rangeOrderPaysNoHookFee (passes in both currency orders). Pool seeded with 1e21 full-range liquidity at LAUNCH_PRICE, elapsed 0 so launchFeeNow() == 0.5e18. ALICE mints liquidity 1e20 in the 1200-tick range just beyond the current tick on the IMD side: she pays 581047279124406 wei IMD and zero SVO; the vault's IMD is unchanged. BOB sells 1e24 SVO through the hooked pool (the vault's IMD rises by his 3.5%). ALICE burns her position: she receives 62753906800716440802742 wei SVO, and the vault's IMD is unchanged by her mint or burn. A 50% buy of the same IMD at that moment would have paid 290523639562203 wei to the vault; she paid 0.","severity":"low","snippet":"The hook fee applies only to the single pool identified by `hook.poolKey()`. Anyone can create and fund another SVO/IMD pool without this hook; trades there pay no fee to this vault and do not use the launch buy-fee decay. A **buy** pays IMD in and receives SVO; a **sell** pays SVO in and receives IMD. Every fee is paid in IMD.","title":"Liquidity operations on the hooked pool pay no hook fee: an IMD-only range order converts IMD to SVO during the 50% launch window without the buy fee (design trade-off, undocumented)"},{"citation":"resolved","description":"afterSwap evaluates _imdBalanceOf(poolManager) unconditionally, before the `if (fee != 0)` branch, so a swap whose IMD leg rounds to a zero fee still reverts (HookCallFailed wrapping InvalidAmount) when IMD's balanceOf reverts or returns fewer than 32 bytes. LifeForceVault._imdBalance() has the same dependency: inferenceReserve(), buybackReserve(), sync(), withdrawInference() and withdrawBuyback() all revert with TransferFailed while balanceOf is unavailable, so the Safe cannot withdraw IMD already held even though the transfer itself might succeed. README 'IMD assumptions and risks' covers a refusing transfer, a false return, a transfer fee and confiscation, but not a reverting or non-standard balanceOf, and MockIMD has no switch for it. A reverting balanceOf would also break the manager's own sync/settle for IMD, so this is a robustness note, not an exploit. Suggested fix: compute asClaim inside `if (fee != 0)` (a zero-fee swap then needs no balance read), add a MockIMD mode whose balanceOf reverts with a test documenting the vault's behaviour, and add the dependency to the README risk list.","line":218,"path":"src/SovrnHook.sol","reproduction":"test/scratch/Judge.t.sol::test_zeroFeeSwapNeedsBalanceOf (passes in both currency orders): SystemBase fixture, seeded pool, 1 hour after opening; vm.mockCallRevert(IMD, abi.encodeWithSignature('balanceOf(address)', manager), 'paused'); then an exact-input sell of 1 wei SVO (IMD output 0, fee 0, nothing to take). Expected: the swap succeeds with a zero IMD delta. Actual: the unlock reverts from afterSwap. Vault side: after sync() of 10e18, mocking balanceOf(vault) to revert makes inferenceReserve() and a Safe call to withdrawInference(1) revert with TransferFailed (LifeForceVault.sol:122).","severity":"info","snippet":"        bool asClaim = _imdBalanceOf(address(poolManager)) < fee;","title":"IMD.balanceOf is a hard dependency of every swap, including zero-fee swaps, and of every vault view and withdrawal; not in the README risk list and not covered by a test"},{"citation":"resolved","description":"Guard still declares error ETHSendFailed and the internal _sendETH low-level value call from the ETH-paired baseline. LifeForceVault, the only contract inheriting Guard, no longer calls it (it uses _sendIMD) and has no receive(); the function is unreachable. It is leftover surface from the fee-currency change this review was asked to scrutinise, it reads as if the vault might still move native ETH (the README says it pays in IMD only), and it costs bytecode in an immutable contract. Merged from three specialist reports. Fix: delete _sendETH and ETHSendFailed from Guard, or reduce Guard to the nonReentrant modifier. No behaviour change.","line":15,"path":"src/Interfaces.sol","reproduction":"grep -rn '_sendETH\\|ETHSendFailed' src test script (excluding test/scratch) returns only src/Interfaces.sol lines 13, 15 and 18: no call site in src/LifeForceVault.sol, src/SovrnHook.sol, the script or any delivered test. The vault uses only nonReentrant from Guard.","severity":"info","snippet":"    function _sendETH(address to, uint256 amount) internal {","title":"ETH-era dead code (_sendETH, ETHSendFailed) remains in Guard and is compiled into the IMD-only vault"},{"citation":"resolved","description":"In the balance < tracked branch, inferenceOut = min(inference, balance) and buybackOut = balance - inferenceOut. If inference <= balance then buybackOut = balance - inference < buyback (because balance < inference + buyback); otherwise buybackOut = 0 <= buyback. The clamp on line 112 can never fire, and in both branches inferenceOut + buybackOut == balance. README line 65 says the sum 'never exceeds the vault's IMD balance and equals it whenever the balance is at least the recorded checkpoints', a weaker property than the code guarantees (always equal). Not a defect. Fix: remove the dead line and state the exact invariant in README so future invariant tests check equality.","line":112,"path":"src/LifeForceVault.sol","reproduction":"Any state: tracked (7e18, 3e18) with balance 9e18 gives views (7e18, 2e18), sum 9e18; with balance 5e18 gives (5e18, 0), sum 5e18. The delivered testFuzz_clampNeverExceedsBalance already asserts inference + buyback == balance for every fuzzed state (forge test --match-test testFuzz_clampNeverExceedsBalance passes), confirming equality rather than only the upper bound the README states.","severity":"info","snippet":"        if (buybackOut > buyback) buybackOut = buyback;","title":"Shortfall clamp on buybackOut is unreachable; reserves always sum exactly to the IMD balance, which the README understates"}],"hash":"2da711c16a8b1432d5d6a2a1dc5ef02388313272f4b4ce72c11f5d50c81c7b12","nodeId":"8825cc19-c32c-48f7-a65e-e2a91bfb5080","outcome":"completed","summary":"The review is complete. The findings file holds nine merged findings, the tracked tree is unchanged, and only ignored scratch files were added.\n\n**Outcome.** No critical or high defect reproduced. The hook's fee mathematics, currency-order handling, quote mechanism, claims path and vault bounds held up under the delivered suite (165 passed, 2 fork suites skipped offline) and under my own scratch tests in both currency orders. Every specialist finding reproduced, so none was dropped, but sixteen specialist entries collapsed into nine.\n\n**Findings written to `.imd-findings.json`, most severe first:**\n\n- **Low, vault checkpoint after a shortfall.** Permissionless sync() and both withdraws persist the clamped reserves, so IMD that later returns is re-split 70/30 and buyback never recovers. Four specialists reported this; merged into one. The attached proof fails on the current code with 9e18 versus 30e18 and passes with the one-line fix I trialled and reverted.\n- **Low, IMD owner powers understated.** Verified on chain: the IMD owner is a codeless single key with an owner-only v4 transfer gate, a blocklist and a bridge. My fork test shows that with the gate on, buys, sells and LP exits all revert while vault-to-Safe withdrawals still work. The README only mentions fee-take reverts.\n- **Low, Safe threshold.** On chain today the REFUEL_SAFE has three owners and threshold 1, so one key can withdraw every fee. The README calls this unverified.\n- **Low, pay-first routers.** The ERC-20 fee take inside afterSwap makes any router that syncs and transfers before swapping revert with CurrencyNotSettled on buys. This is new relative to the ETH version and undocumented.\n- **Low, README contradiction.** Line 111 says live forks were not run while lines 107 and 109 report fork and live-RPC results.\n- **Low, LP fee bypass.** An IMD-only range order acquires SVO without the hook fee during the 50% window. This is a design trade-off, kept because the README does not mention it.\n- **Info:** zero-fee swaps still depend on IMD.balanceOf, dead ETH-era code in Guard, and an unreachable clamp line with the README understating the reserve invariant.\n\n**What was verified and how.** Live reads used `cast` against the Robinhood Chain RPC. Scratch tests live under test/scratch/ and are not kept. Line numbers and snippets for all nine findings were checked mechanically against the tree. This review does not substitute for an independent human audit, and the contracts are not described as audited or secure.","treeHash":null,"usage":{"cachedInputTokens":1592217,"inputTokens":514,"model":"claude-fable-5-1","outputTokens":45410,"runtime":"claude","turns":35,"wallClockMs":1008345}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"789312fc56d3f446","findings":[],"hash":"5b7c21576b25de8da7f307877fa828c9810c136ed69775e261a9e10befe9c420","nodeId":"af172bb6-8523-47dc-8bfa-5bb54502bf5a","outcome":"failed","summary":"wall-clock budget exhausted","treeHash":null,"usage":{"cachedInputTokens":0,"inputTokens":0,"model":"claude-fable-5-1","outputTokens":0,"runtime":"claude","turns":0,"wallClockMs":6937504}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"75052237a39b6e12","findings":[],"hash":"5f2d545d6aabe48e32a542fd48968102af6e164b468ea37c5a11e07f7e43926b","nodeId":"e4b564c2-c286-4ea8-8ef6-6b87012a3c1e","outcome":"failed","summary":"wall-clock budget exhausted","treeHash":null,"usage":{"cachedInputTokens":0,"inputTokens":0,"model":"claude-fable-5-1","outputTokens":0,"runtime":"claude","turns":0,"wallClockMs":6938180}},{"artifacts":[],"attempt":2,"bundleHash":null,"device":"8af9903f4ad1eed0","findings":[{"citation":"resolved","description":"_reserves() has two regimes: when balance >= inference + buyback the untracked surplus is split 30/70 to buyback/inference; when balance < tracked the shortfall is taken from buyback first. The two regimes are only consistent with each other if nothing is checkpointed in between. After a shortfall (an IMD transfer fee, seizure, or pause-then-partial-restore by the IMD owner), the stored checkpoints still hold the old (higher) values; any IMD that then returns to the vault is matched against those old checkpoints and restores buyback first. But sync() is permissionless: if anyone calls it while the balance is short, the clamped values are written to storage, and the same returning IMD is then treated as a fresh receipt and split 30/70. So the final inference/buyback allocation of identical cash flows differs by up to 70% of the recovered amount depending on a third party's sync() call. This is a precision x invariant seam: the invariant 'buyback == floor(0.3 * cumulative receipts) - buyback withdrawals' silently drifts. It only matters if IMD misbehaves (the README covers the shortfall itself but not that the split is path-dependent afterwards), and both reserves pay the same Safe, so funds are not at risk; the earmarking that the README presents as a 70/30 rule is what drifts. Fix: either document that after any shortfall the split of subsequently recovered IMD is undefined, or make sync() record the shortfall explicitly (e.g. keep a 'deficit' counter and apply recovered IMD to the deficit first before splitting any true surplus) so the result is independent of who calls sync() and when.","line":105,"path":"src/LifeForceVault.sol","reproduction":"Mock IMD at its address, chain id 4663, vault from the hook. Path A: transfer 10e18 IMD to the vault, sync() -> (inference 7e18, buyback 3e18); move 3e18 out of the vault (prank the vault, modelling a seizure) -> views (7e18, 0); transfer 2e18 back in -> views (7e18, 2e18). Path B: identical, except ALICE calls vault.sync() while the balance is 7e18 -> stored (7e18, 0); then transfer 2e18 back in -> views (8.4e18, 0.6e18). Same cash flows, buyback reserve 2e18 vs 0.6e18. Reproduced in test/scratch/Audit.t.sol::test_splitDependsOnSyncTimingAfterShortfall (logs: A buyback 2000000000000000000, B buyback 600000000000000000).","severity":"low","snippet":"        if (balance >= tracked) {","title":"Vault 70/30 split after an IMD shortfall depends on whether anyone called sync() before the balance recovers"},{"citation":"resolved","description":"The hook enables only beforeSwap/afterSwap (plus beforeInitialize). modifyLiquidity on the hooked pool itself is unrestricted and carries no hook fee. A participant who wants SVO for IMD can mint an IMD-only position in a tick range just above the current tick and let sellers (who do pay 3.5%) push the price through it; burning the position then returns SVO, acquired at no hook fee and while collecting the 1.25% LP fee. The symmetric trick (SVO-only range below the current tick) sells SVO for IMD with no 3.5% fee. This is an inherent limitation of a swap-only fee hook and is a design trade-off, not a code error, but the README's only statement on fee avoidance is that other pools are unrestricted; it says nothing about liquidity on this pool. The first-hour 50% buy fee in particular is presented as protection of the opening price, and this path bypasses it whenever there is sell flow. Fix: document it in the Fees section (and in launch.json notes), or if the launch policy requires that no path on this pool bypass the buy fee, add beforeAddLiquidity/beforeRemoveLiquidity restrictions (which changes the agreed hook flags and manifest, a scope decision for the requester).","line":40,"path":"README.md","reproduction":"IMD as currency0, pool seeded with 1e21 full-range liquidity at LAUNCH_PRICE (tick 184216), elapsed 0 so launchFeeNow() == 0.5e18. ALICE mints liquidity 1e20 in [184260, 185460] (IMD-only, above the tick): she pays 581047279124406 wei IMD, zero SVO, vault IMD unchanged. BOB sells 1e24 SVO through the hooked pool (pays 3.5% to the vault); tick moves to 185986. ALICE burns her position: receives 62753906800716440802742 wei SVO and 0 IMD back; vault IMD unchanged by her mint or burn. A swap of the same IMD at that moment would have paid 290523639562203 wei (50%) to the vault; she paid 0. Reproduced in test/scratch/Audit.t.sol::test_rangeOrderAcquiresSVOWithoutHookFee.","severity":"low","snippet":"The hook fee applies only to the single pool identified by `hook.poolKey()`. Anyone can create and fund another SVO/IMD pool without this hook; trades there pay no fee to this vault and do not use the launch buy-fee decay. A **buy** pays IMD in and receives SVO; a **sell** pays SVO in and receives IMD. Every fee is paid in IMD.","title":"Liquidity operations on the hooked pool pay no hook fee: a range order converts IMD to SVO during the 50% launch window for free"},{"citation":"resolved","description":"Every fee formula (requested * rate / WAD, actual * rate / (WAD - rate), actual * rate / WAD) rounds toward the trader. The checklist's rule that fees round up is not followed; the leak is strictly less than 1 wei of IMD per swap and the swap's gas exceeds the value by many orders of magnitude, so there is no economic exploit and the README states that integer divisions round down. Recorded for completeness of the math review; no change is required unless the launch policy wants a non-zero fee on every swap, in which case use mulDivRoundingUp for the fee (keeping fee <= requested - 1 on the exact-input buy path so amountToSwap stays non-zero).","line":155,"path":"src/SovrnHook.sol","reproduction":"Seeded pool, warp to openedAt + 3600 (rate 0.035e18). Exact-input buy of 28 wei IMD: fee = 28 * 0.035e18 / 1e18 = 0; vault IMD unchanged, claimFees 0. Exact-input buy of 29 wei: fee 1. Exact-output sell of 28 wei IMD: gross = 28e18/0.965e18 = 29, fee = floor(29 * 0.035) = 1. Reproduced in test/scratch/Audit.t.sol::test_dustBuysPayZeroFee.","severity":"info","snippet":"                fee = requested * rate / WAD;","title":"All hook fee formulas floor, so IMD legs below 29 wei (3.5%) or 2 wei (50%) pay no hook fee"},{"citation":"resolved","description":"In the balance < tracked branch, inferenceOut = min(inference, balance) and buybackOut = balance - inferenceOut. If inference <= balance then buybackOut = balance - inference < buyback (because balance < inference + buyback); if inference > balance then buybackOut = 0 <= buyback. The clamp therefore never fires, and in both branches inferenceOut + buybackOut == balance. The README (Vault accounting) says the sum 'never exceeds the vault's IMD balance and equals it whenever the balance is at least the recorded checkpoints', which understates the actual invariant (always equal). Not a defect; the dead line and the weaker wording can be tightened so readers and future invariant tests check the exact property.","line":112,"path":"src/LifeForceVault.sol","reproduction":"Any state: tracked (7e18, 3e18), balance 9e18 -> (7e18, 2e18), sum 9e18; balance 5e18 -> (5e18, 0), sum 5e18. The existing testFuzz_clampNeverExceedsBalance already asserts a + b == balance for all fuzzed states, confirming equality rather than only an upper bound.","severity":"info","snippet":"        if (buybackOut > buyback) buybackOut = buyback;","title":"Shortfall clamp on buybackOut is unreachable; reserves always sum exactly to the IMD balance"},{"citation":"resolved","description":"Guard still carries _sendETH and ETHSendFailed from the ETH-paired baseline; LifeForceVault no longer calls them (it uses _sendIMD). The function is internal and unreachable, so there is no behavioural effect, but it is dead code in an immutable contract and the README's claim that the vault 'has no receive()' is the only place the ETH path is mentioned. Remove it (or move Guard to a plain reentrancy guard) to keep the audited surface minimal.","line":15,"path":"src/Interfaces.sol","reproduction":"grep -rn '_sendETH\\|ETHSendFailed' src test script returns only src/Interfaces.sol lines 13, 15 and 18: no call site in src/LifeForceVault.sol, src/SovrnHook.sol or any delivered test. The vault uses only the nonReentrant modifier from Guard.","severity":"info","snippet":"    function _sendETH(address to, uint256 amount) internal {","title":"Leftover native-ETH helper from the ETH-paired version is compiled into the vault but unused"}],"hash":"81769c4c8b5d3e40909d2ca9e85f0b1499bf4c3f886131c71c6a2496a38dc8d3","nodeId":"e4b564c2-c286-4ea8-8ef6-6b87012a3c1e","outcome":"completed","summary":"The review is complete. `.imd-findings.json` holds five substantiated findings, none above low. The core math of the ETH-to-IMD generalisation held up under every check I could construct.\n\n## What I verified\n\n**Currency order and fee math (Q1).** For both orders and all four modes, `buy = (zeroForOne == imdIsCurrency0)` and `specifiedIMD = (buy == (amountSpecified < 0))` select the IMD leg correctly, and the manager's own specified/unspecified mapping makes the returned deltas order-agnostic. I proved the two full-fill identities on paper: for a sell exact-output, the quoted gross g satisfies `requested + floor(g*rate/WAD) == g`, so the real swap amount equals the quote; for a buy exact-input partial fill the real input is always at least the quoted consumption, so both swaps stop at the same price limit with the same IMD delta. Fee never exceeds the AMM leg (at 50% it equals it). The delivered fuzz suite against an unhooked reference pool with non-zero protocol fees independently confirms this in both orders.\n\n**Quote mechanism (Q2).** The self-call always reverts, so nested pool state, protocol fees, transient deltas and logs roll back. `busy` cannot be stuck: it is cleared in afterSwap and any revert unwinds it. Reentry from an IMD callback during `take` hits `Busy` on beforeSwap, `AlreadyUnlocked` on a nested unlock, and `onlyManager` on the callbacks. The LP fee is static, so no override path exists.\n\n**ERC-20 fee path (Q3).** Take, claim mint and redemption all net the hook's manager delta to zero. Claims cannot be double-spent; only the hook can burn its claims; `redeemFees` resets atomically. An IMD that reverts or returns false stops trading on the direct path, as the README states.\n\n**Vault (Q4).** Reserves always sum exactly to the IMD balance in both branches, no underflow is reachable, withdrawals are bounded and debit before paying, the guard is correct, and the low-level transfer handles void and false returns.\n\n**Init, token, docs (Q5, Q8, Q9).** Flags 8396 match the five permissions; `validateHookPermissions` rejects wrong-flag addresses. The token is plain, name/symbol/supply exact, burn only to DEAD. README and launch.json numbers and addresses match the code. The attestation check passes. 165 local tests pass in both orders plus 6 fork tests.\n\n## Findings\n\n- **Low, LifeForceVault.sol:105.** After an IMD shortfall, the inference/buyback split of recovered IMD depends on whether a third party called `sync()` in between. Same cash flows yield buyback 2e18 or 0.6e18. Accounting only; both reserves pay the same Safe.\n- **Low, README.md:40.** Liquidity operations on the hooked pool carry no hook fee. A range order acquired SVO worth a 50% swap fee of about 2.9e14 wei IMD for zero hook fee. Design trade-off the README does not mention.\n- **Info, SovrnHook.sol:155.** All fee formulas floor; IMD legs under 29 wei at 3.5% pay nothing. Sub-wei leak, documented as rounding down.\n- **Info, LifeForceVault.sol:112.** The buyback clamp is unreachable and the README understates the invariant.\n- **Info, Interfaces.sol:15.** Dead native-ETH helper left from the ETH version.\n\n## Limits\n\nNo static analysers or the fork test ran. Trust assumptions stand as documented: the IMD owner can halt trading or shrink reserves, the protocol-fee controller can take up to its cap of LP fees, and the Safe has full custody of withdrawals. This review does not substitute for an independent human audit.","treeHash":null,"usage":{"cachedInputTokens":2593787,"inputTokens":610,"model":"claude-fable-5-1","outputTokens":66161,"runtime":"claude","turns":41,"wallClockMs":1459488}},{"artifacts":[],"attempt":2,"bundleHash":null,"device":"26d42bb29b53b9d8","findings":[],"hash":"8cc5dc105d9f6ef9c0e815d31df0eb82d235d13dae32c689b99b749bb8b40b6b","nodeId":"af172bb6-8523-47dc-8bfa-5bb54502bf5a","outcome":"failed","summary":"workspace routing discovery timed out","treeHash":null,"usage":{"cachedInputTokens":0,"inputTokens":0,"model":"gpt-6-astra","outputTokens":0,"runtime":"codex","turns":6,"wallClockMs":1742608}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"f2101c240e0b52a9","findings":[{"citation":"resolved","description":"Line 111 says 'Static analyzers, formal verification and live forks were not run.' Line 109 of the same file reports a result that can only come from a fork run ('The real manager's protocol-fee controller assigned this pool a protocol fee of 0 at the time of the rehearsal'), line 107 reports eth_estimateGas measurements taken against Robinhood Chain on 2026-10-08, and the HEAD commit message is 'Fork rehearsal on real Robinhood Chain (real IMD + real PoolManager, both orders)'. One of the two statements is wrong. The task's wording rule is that every README claim must match exactly; a reader deciding how much live evidence exists is told two incompatible things. The fork suite itself cannot be checked offline: without FORK_4663_RPC both Fork4663 contracts are skipped (165 passed, 2 skipped), so this review could not confirm the line-109 claim either.","line":111,"path":"README.md","reproduction":"Input: read README.md lines 107, 109 and 111 together. Expected: a single consistent statement of what was and was not run on a live fork. Actual: line 111 denies that any live fork was run; lines 107 and 109 report results obtained from a live fork / live RPC. Offline, `forge test` shows Fork4663ImdLowTest and Fork4663ImdHighTest skipped, so neither claim is verifiable from this tree alone. Fix: replace the sentence on line 111 with an exact statement, e.g. 'Static analyzers and formal verification were not run. A fork rehearsal (test/Fork4663.t.sol) was run on <date> against <RPC>; it is skipped in a default `forge test` and must be re-run before launch.'","severity":"low","snippet":"This deliverable includes no deployment transactions. Local tests and source review do not establish live-chain readiness. A separate independent adversarial review and a chain-specific deployment rehearsal remain with the launch process; no external security certification is asserted. Static analyzers, formal verification and live forks were not run. `test/REVIEW.md` and `test/README.md` are historical records of the ETH-paired review.","title":"README states live forks were not run while the same README reports fork-rehearsal and real-chain results"},{"citation":"resolved","description":"The shortfall branch of _reserves() gives inference priority (documented, README line 65). What is not documented is the interaction with the permissionless sync(): if the balance dips and anyone calls sync() during the dip, the lowered buyback checkpoint becomes permanent, and when the balance later recovers the recovered amount is treated as new IMD and split 70/30 again. Buyback never returns to its pre-dip share. This needs an IMD whose balances can fall and rise without a transfer by the vault (a negative-then-positive rebase, or an owner seizure that is later reversed). The fork test asserts the current IMD moves exactly, so this is latent; it is listed because the README's 'IMD assumptions and risks' section names seizure as a modelled case but does not state the one-way effect. No funds leave the vault and the Safe's total withdrawable amount is unchanged; only the inference/buyback accounting label moves. This is a documented-design trade-off with an undocumented corner, not a theft path.","line":110,"path":"src/LifeForceVault.sol","reproduction":"State: vault holds 10 IMD, sync() called -> inference 7, buyback 3. IMD balance of the vault falls by 2 (seizure or negative rebase) -> views 7 / 1. Anyone calls sync() -> checkpoints 7 / 1. Balance recovers by 2 -> views 8.4 / 1.6 (the 2 is split floor 70/30 on top of the 7 / 1 checkpoint). Expected under the stated 70/30 model of the 10 IMD held: 7 / 3. Actual: 8.4 / 1.6. Reproduced in test/scratch/VaultRecovery.t.sol (passes against current code, asserting the 8.4 / 1.6 outcome). Fix options: document the one-way effect in README 'Vault accounting and callers', or in sync() only checkpoint when balance >= inference + buyback so a dip is never persisted (withdrawals would still clamp correctly because _reserves() reads the live balance).","severity":"info","snippet":"        inferenceOut = inference > balance ? balance : inference;\n        buybackOut = balance - inferenceOut;\n        if (buybackOut > buyback) buybackOut = buyback;","title":"After a temporary IMD shortfall any permissionless sync() permanently re-splits recovered IMD 70/30 instead of restoring buyback"},{"citation":"resolved","description":"In the ETH-paired version the fee left the manager as native value, which PoolManager settles by msg.value. With IMD, the manager settles ERC-20 input as (balance after transfer) - (balance recorded by sync()). The hook now moves `fee` IMD out of the manager during afterSwap. A router that calls sync(IMD), transfers the input, then swaps, then settle() (sync held open across the swap) is credited `input - fee` by settle() and ends the unlock with an unsettled -fee delta: the swap reverts with CurrencyNotSettled. The same router and order succeed on a hookless pool and on sells (the fee is on the output side). Uniswap's V4Router/Universal Router sync immediately before transfer+settle after the swap, so they are not affected; a custom integrator could be. The claims fallback does not have this property (mint moves no tokens). This is a property of any hook that takes mid-swap and is not a correctness defect in the fee logic, but it is new to the IMD variant and the README's 'Fees and settlement' section does not mention it.","line":224,"path":"src/SovrnHook.sol","reproduction":"Setup: hooked pool and an identical hookless pool, full-range liquidity, past the decay window. Router A order: sync(IMD) -> transferFrom(payer, manager, 1e18) -> swap(zeroForOne buy, -1e18) -> settle() -> take(SVO). On the hookless pool: succeeds. On the hooked pool with order swap -> sync -> transfer -> settle: succeeds. On the hooked pool with the sync held open across the swap: reverts IPoolManager.CurrencyNotSettled, because settle() credits 1e18 - 0.035e18 while the trader's delta is -1e18. Same router, sell exact-input of 1000e18 SVO with sync held open: succeeds. Reproduced in test/scratch/PreSyncRouter.t.sol (passes on current code, asserting the revert). Fix: document in README that integrators must settle IMD input either after the swap or with sync()+settle() completed before the swap; or, if full compatibility is wanted, always mint ERC-6909 claims and redeem them permissionlessly (a design change that moves every fee through redeemFees()).","severity":"info","snippet":"                poolManager.take(Currency.wrap(IMD), address(vault), fee);","title":"Direct fee take inside afterSwap breaks routers that hold an IMD sync open across the swap (buys only); undocumented integration constraint new to the ERC-20 fee path"}],"hash":"c59a5dd4d4fa14c6da9fea803efe888d9ec628d8714ebd55e9729f0d6c54d3b7","nodeId":"b216b7d3-929b-4611-8dc5-7d245836280d","outcome":"completed","summary":"The review is complete. Both deliverables are in place and nothing tracked by git was changed.\n\n**Outcome.** No critical, high or medium defect was found in the ETH-to-IMD generalisation or elsewhere in scope. Three items are recorded in `.imd-findings.json`, each with a concrete failing input and a snippet verified against the tree:\n\n- **Low, README.md:111.** The README says live forks were not run, while lines 107 and 109 report real-chain gas estimates and a fork-rehearsal result. Offline both fork suites are skipped, so neither claim is checkable here.\n- **Info, LifeForceVault.sol:110.** After a temporary balance dip, any permissionless `sync()` makes the lowered buyback checkpoint permanent. Recovered IMD is re-split 70/30, so 10 IMD at 7/3 ends at 8.4/1.6. Needs a rebasing or seize-and-restore IMD. No funds move.\n- **Info, SovrnHook.sol:224.** Taking the fee as an ERC-20 mid-swap breaks a router that holds `sync(IMD)` open across the swap, buys only, with `CurrencyNotSettled`. Uniswap's routers settle after the swap and are unaffected. This constraint is new to the IMD variant and undocumented.\n\n**What was checked and held.** Fee direction and IMD-leg selection for both currency orders and all four modes, including the exact-output sell identity `requested + floor(gross*rate/WAD) == gross` and partial-fill matching between quote and real swap. A fresh fuzz of 4,000 runs per order with a 0.1% protocol fee and crossed tick ranges produced no QuoteMismatch, no revert on a valid swap, and fee always equal to the stated rate of the actual IMD leg. Quote rollback, the busy flag, claims accounting, redemption reentrancy, the vault clamp and guard, the hook flag value 8396, the constructor guards, and the token's plainness all matched the README and manifest.\n\n**Trust assumptions stated in the report.** A vault blacklist by the IMD owner halts all trading on the hooked pool, since the live manager always holds enough IMD for the direct path. A reverting `balanceOf` also freezes the vault's views and withdrawals, which the README does not mention. The Safe can withdraw the whole balance; the 70/30 split is accounting only.\n\n**Not run.** Static analysers, formal verification, the fork suites, and live gas estimation. The real IMD bytecode was not inspected. `report.md` at the repository root has the per-question verdicts, evidence, coverage gaps, and the method applied, and states that this review does not substitute for an independent human audit.","treeHash":null,"usage":{"cachedInputTokens":2700456,"inputTokens":450,"model":"claude-fable-5-1","outputTokens":71634,"runtime":"claude","turns":64,"wallClockMs":1322106}},{"artifacts":[],"attempt":3,"bundleHash":null,"device":"2dc755dfe7bd177c","findings":[{"citation":"resolved","description":"The vault derives reserves from IMD.balanceOf with a checkpoint model. When the balance is below the recorded checkpoints, _reserves() clamps buyback first (the documented priority). sync() and both withdraw functions then OVERWRITE the stored checkpoints with the clamped values, so the buyback checkpoint that was only temporarily unbacked is deleted. If the missing IMD later returns (or new fees arrive), the whole returning amount is treated as new IMD and re-split 70/30 on top of the already-full inference checkpoint. Because sync() is callable by anyone, any third party can choose the moment of the checkpoint and thereby permanently rewrite the 70/30 accounting in favour of inference. The README (section 'Vault accounting and callers') says only that a shortfall 'reduces buyback first'; it does not say the reduction becomes permanent once anybody calls sync(), nor that the final split is path dependent on who calls sync() when. The preconditions are IMD-level (a transfer fee, a seizure, a pause that is later lifted, or any route that moves IMD out of the vault and back), which the README itself lists as possible; both reserves are paid to the same Safe, so no funds leave the system, which is why this is low. Fix (minimal, preserves the design): keep the clamp in the views but do not write clamped values back as checkpoints. In sync() and the withdraw functions only raise checkpoints when balance >= tracked (the 'extra' case); when balance < tracked leave inference/buyback as they are and bound the withdrawal by the clamped view. Alternatively record the shortfall separately (a 'deficit' that is repaid before any new IMD is split).","line":63,"path":"src/LifeForceVault.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\nimport {Test} from \"forge-std/Test.sol\";\nimport {PoolManager} from \"v4-core/src/PoolManager.sol\";\nimport {IPoolManager} from \"v4-core/src/interfaces/IPoolManager.sol\";\nimport {SovrnToken} from \"src/SovrnToken.sol\";\nimport {LifeForceVault} from \"src/LifeForceVault.sol\";\nimport {ERC20} from \"solmate/src/tokens/ERC20.sol\";\n\ncontract PlainIMD is ERC20 {\n    constructor() ERC20(\"Identity.md\", \"IMD\", 18) {\n        _mint(msg.sender, 1e30);\n    }\n}\n\n/// @dev A temporary shortfall (IMD leaves the vault by a route the vault does not control, then the same\n///      amount comes back) must not change the 70/30 split. Today anyone can call sync() during the\n///      shortfall and permanently move the buyback reserve into inference.\ncontract VaultSyncShortfallTest is Test {\n    address constant IMD_ADDR = 0x5F7Bb59365ce557C26dbcAa4EE9d39A4b95B7127;\n    address constant BOB = address(0xB0B);\n    PlainIMD imd;\n    LifeForceVault vault;\n\n    function setUp() public {\n        vm.chainId(4663);\n        PoolManager manager = new PoolManager(address(this));\n        deployCodeTo(\"VaultSyncShortfall.t.sol:PlainIMD\", \"\", IMD_ADDR);\n        imd = PlainIMD(IMD_ADDR);\n        SovrnToken token = new SovrnToken();\n        vault = new LifeForceVault(IPoolManager(address(manager)), token, address(0xBEEF));\n    }\n\n    function test_permissionlessSyncDuringShortfallRewritesSplit() public {\n        imd.transfer(address(vault), 100 ether);\n        vault.sync();\n        assertEq(vault.inferenceReserve(), 70 ether);\n        assertEq(vault.buybackReserve(), 30 ether);\n\n        // 30 IMD leave the vault by a route the vault does not control (modelled with a prank).\n        vm.prank(address(vault));\n        imd.transfer(BOB, 30 ether);\n        assertEq(vault.inferenceReserve(), 70 ether);\n        assertEq(vault.buybackReserve(), 0);\n\n        // Anyone checkpoints while the balance is short.\n        vm.prank(BOB);\n        vault.sync();\n\n        // The same 30 IMD come back.\n        vm.prank(BOB);\n        imd.transfer(address(vault), 30 ether);\n\n        // Expected: the original 70 / 30 is restored (the balance equals the original checkpoint again).\n        // Actual: 91 / 9, because the sync() during the shortfall deleted the buyback checkpoint and\n        // the returned 30 IMD is re-split 70/30 on top of inference.\n        assertEq(vault.buybackReserve(), 30 ether, \"buyback reserve permanently reduced by a permissionless sync\");\n        assertEq(vault.inferenceReserve(), 70 ether);\n    }\n}","reproduction":"Deploy LifeForceVault with a plain ERC-20 at the IMD address (chain id 4663). 1) transfer 100 IMD to the vault, call sync(): inferenceReserve()=70, buybackReserve()=30. 2) Move 30 IMD out of the vault by a route the vault does not control (modelled with vm.prank(vault) imd.transfer(BOB, 30)): views now 70 / 0. 3) Any address calls vault.sync(). 4) BOB transfers the same 30 IMD back to the vault. Expected: balance equals the original checkpoint again, so 70 / 30. Actual: inferenceReserve()=91 ether, buybackReserve()=9 ether. Without step 3 the views return 70 / 30. Run: forge test --match-path test/scratch/VaultSyncShortfall.t.sol (fails on the current code with '9000000000000000000 != 30000000000000000000').","severity":"low","snippet":"    function sync() external nonReentrant {\n        uint256 tracked = inference + buyback;\n        (uint256 newInference, uint256 newBuyback) = _reserves();\n        uint256 seen = newInference + newBuyback;\n        inference = newInference;\n        buyback = newBuyback;","title":"Permissionless sync() during an IMD balance shortfall permanently moves buyback reserve into inference"},{"citation":"resolved","description":"afterSwap evaluates _imdBalanceOf(address(poolManager)) unconditionally, before the `if (fee != 0)` branch, so a swap whose IMD leg rounds to zero fee still reverts (HookCallFailed wrapping InvalidAmount) when IMD's balanceOf reverts or returns fewer than 32 bytes. LifeForceVault._imdBalance() has the same dependency: inferenceReserve(), buybackReserve(), sync(), withdrawInference() and withdrawBuyback() all revert with TransferFailed while balanceOf is unavailable, so the Safe cannot withdraw even IMD already held (a transfer that would succeed is never attempted). The README's 'IMD assumptions and risks' section covers a refusing transfer, a false-returning transfer, a transfer fee and confiscation, but not a reverting or non-standard balanceOf, and MockIMD has no switch for it, so no test in the suite exercises this path. This is a trust/robustness note rather than an exploitable defect: a reverting balanceOf would also break the PoolManager's own sync/settle for IMD. Suggested fix: move the balance check inside `if (fee != 0)` in afterSwap (saves a staticcall on zero-fee swaps and lets them proceed), add a MockIMD mode whose balanceOf reverts with a test that documents the vault's behaviour, and add the dependency to the README risk list.","line":218,"path":"src/SovrnHook.sol","reproduction":"Fixture: PoolManager, plain ERC-20 at 0x5F7Bb59365ce557C26dbcAa4EE9d39A4b95B7127, SovrnToken at an address above IMD, hook at 0x20cc, pool initialized and seeded with full-range liquidity 1e22. vm.mockCallRevert(IMD, abi.encodeWithSignature('balanceOf(address)', manager), 'paused'); then an exact-input sell of 1 wei SVO (zeroForOne=false, amountSpecified=-1, limit MAX_SQRT_PRICE-1). Expected (fee is 0, nothing is taken): the swap succeeds with IMD delta 0. Actual: the unlock reverts with Hooks.HookCallFailed wrapping SovrnHook.InvalidAmount from afterSwap. Vault: after sync() of 10 IMD, mock balanceOf(vault) to revert; inferenceReserve() and a Safe call to withdrawInference(1) both revert with TransferFailed. See test/scratch/BalanceOfRevert.t.sol (both tests pass, i.e. they demonstrate the behaviour).","severity":"info","snippet":"        bool asClaim = _imdBalanceOf(address(poolManager)) < fee;","title":"IMD.balanceOf is a hard dependency of every swap (including zero-fee swaps) and of every vault view and withdrawal; not listed in the README's IMD risk list and not covered by a test"},{"citation":"resolved","description":"Guard still carries `error ETHSendFailed()` and `_sendETH`, which nothing in the IMD-paired code calls (grep shows no caller in src/). LifeForceVault inherits Guard only for nonReentrant. The dead code is compiled into the vault's ABI as an error and increases the audit surface and the attestation hash without purpose; it also reads as if the vault might still move native ETH, which the README says it does not. Fix: delete `_sendETH` and `ETHSendFailed` from Guard (or reduce Guard to the reentrancy modifier). No behaviour change.","line":13,"path":"src/Interfaces.sol","reproduction":"grep -n '_sendETH\\|ETHSendFailed' src/*.sol returns only the definition at src/Interfaces.sol:13-19; there is no call site. forge build succeeds with the two members removed.","severity":"info","snippet":"    error ETHSendFailed();\n\n    function _sendETH(address to, uint256 amount) internal {\n        if (amount == 0) return;\n        (bool ok,) = to.call{value: amount}(\"\");\n        if (!ok) revert ETHSendFailed();\n    }","title":"Dead native-ETH helper left over from the ETH-paired base in the vault's Guard base contract"}],"hash":"ce6c0b4c904ec4f11e336835cb5c1b846a12f50bf1eed80d2ac58a9db5a88320","nodeId":"af172bb6-8523-47dc-8bfa-5bb54502bf5a","outcome":"completed","summary":"The review is complete. `.imd-findings.json` is written at the repository root with three substantiated findings, and no repository file was modified.\n\n**What I checked.** The full suite builds and passes (165 passed, 2 fork tests skipped without `FORK_4663_RPC`), `forge fmt --check` and `python3 script/attest.py --check` pass. I traced the hook against the vendored v4 `Hooks` and `PoolManager` code for all four swap modes in both currency orders: `buy = (zeroForOne == imdIsCurrency0)`, `specifiedIMD`, the `amount0`/`amount1` leg selection, the `toBalanceDelta` orientation in `Hooks.afterSwap`, the exact-output sell identity `requested + floor(gross*rate/WAD) == gross`, and the partial-fill cases where the real swap's `amountToSwap` differs from the quoted one. All reconcile. The quote self-call is skipped by the manager because `msg.sender == hook`, and every reverted nested swap rolls back storage, transient deltas and logs. Claims mint/burn/take accounting nets to zero, `redeemFees` cannot be re-entered or double-spent, the vault clamp cannot exceed the balance or underflow, `_sendIMD` handles reverts, false returns and short return data, the token is plain with the stated name, symbol and supply, flags 8396 match the five permissions, and the README and manifest numbers match the code. No critical, high or medium defect was reproduced.\n\n**Findings recorded.**\n- **Low, `src/LifeForceVault.sol:63`.** During an IMD balance shortfall, a permissionless `sync()` overwrites the checkpoints with clamped values, so when the IMD returns the buyback reserve is permanently re-split toward inference. Reproduced in `test/scratch/VaultSyncShortfall.t.sol`, which fails on the current code with buyback 9 IMD instead of 30. Proof source is attached. Fix suggested: do not write clamped values back as checkpoints.\n- **Info, `src/SovrnHook.sol:218`.** `IMD.balanceOf` is called on every swap before the `fee != 0` check, so even a zero-fee swap reverts if `balanceOf` is unavailable, and all vault views and withdrawals share that dependency. Not in the README risk list and not covered by any test. Demonstrated in `test/scratch/BalanceOfRevert.t.sol`.\n- **Info, `src/Interfaces.sol:13`.** Dead `_sendETH` and `ETHSendFailed` left over from the ETH-paired base.\n\n**Limits.** This was a single-reviewer source review with local Foundry tests only. No static analysers, formal tools or the live fork were run, and the real IMD token's transfer rules and the Safe's control on chain 4663 remain unverified. It does not replace an independent human audit.","treeHash":null,"usage":{"cachedInputTokens":1130726,"inputTokens":354,"model":"claude-fable-5-1","outputTokens":50574,"runtime":"claude","turns":33,"wallClockMs":870753}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"2a9662a76cb5f51d","findings":[{"citation":"resolved","description":"README lines 30-36 and the launch.json notes describe IMD's rules only hypothetically ('a blacklist, a pause, a transfer hook'). The deployed IMD at 0x5F7Bb59365ce557C26dbcAa4EE9d39A4b95B7127 on chain 4663 is a non-proxy contract (EIP-1967 slot empty, ~15 KB runtime) with these verifiable features, read from its bytecode selectors and revert strings and from eth_call: (a) a LayerZero OFT (lzReceive, setPeer, peers; live peers for eids 30101 and 30184), so the owner can point a peer at a contract it controls and credit unbounded IMD on 4663 through lzReceive; (b) a one-way transfer switch (transfersEnabled() = true, enableTransfers() reverts 'BridgedFP: already enabled'); (c) a blocklist view blocked(address) (currently false for the PoolManager and the Safe); (d) an owner-only setV4Config(address pm, address gate, bool) with poolManager() currently 0x0 and a gate-only approval function (selector 0x059c9548 reverts 'BridgedFP: not v4 gate'). When the gate is on, IMD transfers out of the PoolManager revert 'BridgedFP: v4 transfer not approved' unless the gate approves. (e) owner() = 0x047F606fD5b2BaA5f5C6c4aB8958E45CB6B054B7 has no code: a single externally owned key. Consequences for this launch that the README does not state: with the gate on and this pool not approved, every fee-paying swap reverts (the hook's take of IMD to the vault at SovrnHook.sol:224 is a transfer from the manager) AND liquidity providers cannot remove IMD either (modifyLiquidity's take is the same transfer), so both trading and LP exit halt, and the claims fallback cannot help because redeemFees uses the same take. The README's 'Trading on the hooked pool stops while that lasts' covers only the vault-blacklist case and omits that LP exit is also blocked and that the owner is one EOA. The vault's own withdrawals to the Safe are unaffected by the gate (they are not PoolManager transfers) unless the vault or Safe is blocklisted. Fix (documentation and launch process, no code change): name these mechanisms and the EOA owner in README 'IMD assumptions and risks' and in launch.json notes; before launch, have the launch process confirm with the IMD owner whether the v4 gate will be enabled and, if so, that this pool's PoolManager transfers (fee take to the vault, LP take) are approved by the gate; record the blocked() status of the vault address after deployment.","line":32,"path":"README.md","reproduction":"Live chain reads (RPC https://rpc.mainnet.chain.robinhood.com): cast call IMD 'owner()(address)' -> 0x047F606f...; cast code 0x047F606f... -> 0x (EOA); cast call IMD 'poolManager()(address)' -> 0x0; cast call IMD 'transfersEnabled()(bool)' -> true; cast call --from <owner> IMD 'setV4Config(address,address,bool)' 0x8366a39C...40951 0xdEaD true -> succeeds; same from 0x1111..1111 -> OwnableUnauthorizedAccount. Fork reproduction (forge, FORK_4663_RPC set): deploy the hook+vault+token on a fork, initialize the real-manager pool, seed liquidity, buy 1 IMD -> fee reaches the vault. Then vm.prank(imd.owner()); imd.setV4Config(0x8366a39CC670B4001A1121B8F6A443A643e40951, address(0xdead01), true); the same buy now reverts with Wrap__FailedHookCall wrapping 'BridgedFP: v4 transfer not approved' (the hook's take at SovrnHook.sol:224), and router.liquidity(key, ModifyLiquidityParams(-887220, 887220, -1e21, 0)) also reverts with the same IMD error on the LP's take. Expected per README: only 'a swap whose fee is taken directly reverts'. Actual: swaps and LP IMD withdrawals both revert, switchable by one key.","severity":"low","snippet":"IMD is an independent token with its own owner and rules, which this code does not control. The contracts treat it as a plain ERC-20 and are written so that an unusual IMD cannot make them credit more than they hold, but they cannot prevent it from hurting the pool:","title":"IMD owner powers understated: on-chain IMD has an owner-switchable v4 transfer gate, a blocklist and a bridge-mint path; the gate halts this pool's swaps and LP exits (trust assumption, not a code def"},{"citation":"resolved","description":"LifeForceVault.withdrawInference/withdrawBuyback (LifeForceVault.sol:76-90) pay only REFUEL_SAFE = 0xEb57c52272B90F989C41B739e2ccc5f00bF7697C and both reserves together equal the vault's whole IMD balance, so the Safe's signing policy is the only control over 100% of collected fees. The README and launch.json say the threshold is unverified. It is verifiable: on chain 4663 that address holds a Safe proxy (masterCopy 0xEdd160fEBBD92E350D4D398fb636302fccd67C7e, VERSION 1.5.0) with getOwners() = [0x217C05f5D1D1E595BBae94534540B803bfC4563B, 0xb1eC9d1C36974d05eb9889eBf8A150b05791E559, 0x7fFA8901be4777D9fD78F9A00D98DFCDBD833671] (all three without code) and getThreshold() = 1. One compromised or rogue key among three suffices to withdraw all inference and buyback IMD, and the contracts have no rate limit, delay or alternative recipient. Not a code defect: the single-recipient design is intended. Fix (operational, before launch): raise the Safe threshold to at least 2-of-3, or document in README/launch.json that custody is effectively single-key; the code need not change.","line":97,"path":"README.md","reproduction":"cast call 0xEb57c52272B90F989C41B739e2ccc5f00bF7697C 'getThreshold()(uint256)' --rpc-url https://rpc.mainnet.chain.robinhood.com -> 1; 'getOwners()(address[])' -> the three addresses above; 'VERSION()(string)' -> 1.5.0. Then, with any one owner key, execTransaction -> vault.withdrawInference(inferenceReserve()) and vault.withdrawBuyback(buybackReserve()) drains the vault to the Safe (the vault only checks msg.sender == REFUEL_SAFE, LifeForceVault.sol:41-44). Expected per README: operators confirm a threshold before launch; actual on-chain state today: threshold 1.","severity":"low","snippet":"After launch, no setters or setup transactions exist. The Safe operators monitor both reserves and any claim backing, arrange permissionless redemption when needed, withdraw inference funds in IMD (and sell it for the USD that the voice provider requires), and withdraw buyback funds to acquire SVO by hand with appropriate trade limits. They transfer acquired SVO to the vault and anyone calls `burn()`. The contracts enforce the withdrawal destination and reserve bounds; they cannot enforce what the Safe does with withdrawn IMD or schedule its purchases. Operators must confirm control of the specified Safe on chain 4663, including its signing threshold, before launch. The supplied Safe identity is a requester parameter, not independently verified here.","title":"REFUEL_SAFE on chain 4663 is a 1-of-3 Safe: any single owner key can withdraw every fee IMD (trust assumption; README leaves the threshold unverified)"},{"citation":"resolved","description":"In the ETH-paired baseline the fee was taken as native currency, and native settlement uses msg.value, so a router's settle did not depend on the manager's balance. With IMD as the fee currency, afterSwap now executes PoolManager.take(IMD, vault, fee) in the middle of the swap. ERC-20 settlement in v4 is a balance difference: sync(IMD) records the manager's balance, then settle() credits balanceNow - recorded. A router that syncs and transfers the input before calling swap (a valid v4 ordering, used by some integrations and by any flow that pre-pays) is credited fee less than it transferred because the hook moved fee IMD out between its sync and its settle; the unlock ends with a non-zero delta and reverts (CurrencyNotSettled), or the router must overpay by fee. Routers that swap first and then sync/settle (Uniswap's V4Router and Universal Router) are unaffected. Impact: no fund loss, but a class of otherwise valid integrations cannot trade on the hooked pool, and the README does not mention this integrator constraint (it is new relative to the imported ETH version). Fix: document in README 'Fees and settlement' that the hook takes IMD during afterSwap, so integrators must settle IMD after the swap (sync immediately before settle), or alternatively mint the fee as ERC-6909 claims to the hook unconditionally and have redeemFees take them later (a design change: fees would then always need a redemption step).","line":224,"path":"src/SovrnHook.sol","reproduction":"Scratch test (local, mock plain ERC-20 at IMD's address, chain id 4663, real vendored PoolManager, IMD as currency0, full-range liquidity 1e22 on a hooked pool and on an identical hookless pool, 1 hour after opening): a PayFirstRouter whose unlockCallback does m.sync(IMD); IMD.transferFrom(payer, manager, 1e18); d = m.swap(key, SwapParams(true, -1e18, MIN_SQRT_PRICE+1)); m.settle(); m.take(SVO, payer, out). On the hookless key the call succeeds. On the hooked key the same call reverts (settle credits 1e18 - 0.035e18, leaving the router 0.035e18 short). Expected: a v4-valid pay-first swap settles; actual: revert only on the hooked pool. Test passed as written: test_payFirstRouterWorksOnHooklessPoolButNotOnHookedPool.","severity":"low","snippet":"                poolManager.take(Currency.wrap(IMD), address(vault), fee);","title":"ETH-to-IMD change: the in-swap take of IMD to the vault breaks any router that pays the input before swapping (sync -> transfer -> swap -> settle), a flow that worked with native ETH"},{"citation":"resolved","description":"_reserves() (LifeForceVault.sol:102-113) clamps the views to the real balance without touching storage, so a transient shortfall (IMD removed from the vault by any route the vault does not control) is reversible: when the balance returns, the views recover the original 70/30 checkpoint. sync() is callable by anyone and writes the clamped values into storage (inference, buyback = clamped). If a stranger calls sync() while the balance is below the checkpoint, the buyback write-down becomes permanent and IMD restored afterwards is treated as new income and re-split 70/30, moving up to the whole shortfall from buyback to inference. Both reserves pay the same Safe, so no value leaves the system; only the inference/buyback labelling changes, and the README says the split is accounting only. Reachable only if IMD ever reduces the vault's balance without a Safe withdrawal (a seizure or a transfer-fee token); the deployed IMD shows no fee and no seizure function in its selectors, so this is a documented trade-off rather than a live risk. Fix if the labelling matters: in sync(), only checkpoint when balance >= tracked (skip the write-down), or keep the shortfall as a separate pending-loss counter so a later restoration refills buyback first.","line":63,"path":"src/LifeForceVault.sol","reproduction":"Local scratch test with MockIMD: transfer 10e18 to the vault, sync() -> checkpoint 7e18/3e18; vm.prank(vault) imd.transfer(0xBEEF, 3e18) -> views 7e18/0. Case A: 0xBEEF returns 3e18 -> views 7e18/3e18. Case B: 0xA11CE calls sync() first (checkpoint becomes 7e18/0), then 0xBEEF returns 3e18 -> views 9.1e18/0.9e18. Same funds, different reserves depending on an unprivileged call. Both scratch tests passed (test_withoutSyncRestoreRecoversBuyback, test_strangerSyncBeforeRestoreShiftsBuybackToInference).","severity":"info","snippet":"    function sync() external nonReentrant {","title":"Permissionless sync() during an IMD shortfall permanently re-labels buyback IMD as inference once the balance is restored"},{"citation":"resolved","description":"Guard still declares error ETHSendFailed and internal _sendETH (a low-level value call) from the ETH-paired baseline. LifeForceVault (the only user of Guard) no longer calls it; the vault has no receive() and never holds ETH by design. The function is unreachable, so there is no exploit, but it is leftover surface from the fee-currency change the review was asked to scrutinise, it contradicts the README statement that the vault pays 'in IMD only' at the source level, and it costs bytecode. Fix: delete _sendETH and ETHSendFailed from Guard (or reduce Guard to the nonReentrant modifier). No behaviour change.","line":15,"path":"src/Interfaces.sol","reproduction":"grep -n '_sendETH\\|ETHSendFailed' src/ -> only the declaration in src/Interfaces.sol:13-19; no call site in src/LifeForceVault.sol or src/SovrnHook.sol. forge build succeeds with the function removed (it is referenced nowhere).","severity":"info","snippet":"    function _sendETH(address to, uint256 amount) internal {","title":"ETH-era dead code (_sendETH, ETHSendFailed) remains in Guard and is compiled into the IMD-only vault"}],"hash":"cff631e9d03d4ba2645dd8b3499e5e44971402c2c732b4a1e7c295bd39b27d35","nodeId":"0c3d593d-7645-46dd-a8b9-3fa79d309428","outcome":"completed","summary":"The review is complete. The findings file is written and validated, the scratch tests are removed, and the working tree holds only the deliverable.\n\n**Outcome.** No critical, high or medium code defects were found in the hook, vault or token. The findings file holds five items: three low (all trust or integration assumptions, not code bugs) and two info. Nothing here substitutes for an independent human audit, and the contracts are not called audited or secure.\n\n**What I verified, per hard question.**\n\n- **Currency order and fee math.** The buy/specified derivations and every sign and return delta are correct in both orders. I proved the exact-output sell identity (requested plus floor(gross times rate) equals the quoted gross) and the exact-input partial-fill consistency on paper, then ran a 2,500-run fuzz per order over prices from 1e-10 to 1e10, amounts 1 to 1e30, all four modes, random price limits, with and without a protocol fee. No QuoteMismatch, Busy or Unauthorized revert on an AMM-valid swap, and every fee equalled floor(gross times rate / WAD).\n- **Quote mechanism.** The self-call reverts all nested state including transient slots and protocol fees. The busy flag blocks swap reentry from an IMD callback and from redeemFees. The pool uses a static LP fee, so no override path exists. Nested unlock is refused by the manager.\n- **ERC-20 fee path.** The manager-balance check is sufficient for a plain token. Claims are backed by the swapper's settlement and cannot be double-spent. One new behaviour from the ETH-to-IMD change is recorded as a low finding: the in-swap take breaks routers that pay before swapping, which native settlement tolerated.\n- **Vault accounting.** Reserves always sum to exactly the balance, never underflow, and withdrawals follow checks-effects-interactions behind a working guard. The low-level transfer handling is safe for no-return and false-return tokens. The only asymmetry found is the permissionless checkpoint timing during a shortfall (info).\n- **Initialisation and addresses.** The guards are complete. Flag validation in the constructor rejects any mis-mined address, and the vault is created only after that check.\n- **Launch window.** The zero-liquidity price move is real and already documented in the README.\n- **Trust.** This is where the material results are. On chain 4663 today, IMD is a LayerZero OFT with an owner-switchable v4 transfer gate, a blocklist and bridge mint, owned by a single EOA. On a fork I enabled the gate as the owner: every fee-paying swap and every LP withdrawal of IMD then reverts with \"BridgedFP: v4 transfer not approved\". The README describes these powers only hypothetically. The Safe exists and is a Safe v1.5.0 with threshold 1 of 3, which the README leaves as unverified.\n- **Token.** Plain, name and symbol exact, supply 10^27, burn sends only SVO only to DEAD.\n- **Tests and docs.** The repository's own fork suite passes against the live chain (protocol fee 0, LP fee 12500). Every number, address and privilege claim I checked in README and launch.json matches the code. The suite covers both orders and the misbehaving-IMD mocks; it does not cover a pay-first router or the live gate.\n\n**Open items for the launch process, not for code:** confirm with the IMD owner whether the v4 gate will be enabled and that this pool is approved if so, and raise or explicitly accept the Safe's 1-of-3 threshold.\n\nSources consulted for the IMD source search returned nothing relevant: [docs.bags.fm](https://docs.bags.fm/api-reference/get-rh-token.md), [docs.blockdaemon.com](https://docs.blockdaemon.com/docs/robinhood.md), [github.com/hoodstack](https://github.com/hoodstack), [docs.getblock.io](https://docs.getblock.io/api-reference/robinhood.md). The explorer API also returned no verified source, so IMD's behaviour was established from its bytecode, revert strings and live simulation.","treeHash":null,"usage":{"cachedInputTokens":2406526,"inputTokens":482,"model":"claude-fable-5-1","outputTokens":71859,"runtime":"claude","turns":56,"wallClockMs":1404329}}],"verification":[]}