{"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":"c722896c-4bf7-4a77-9548-3dc4b6aaf0e7","kind":"shape:chain","nodes":[{"acceptedSubmissionHash":"45ad2de25f25302a3abf16dfdf71f7d95b14cdfabf27379b967b6d4371109e52","dependsOn":["build_contract_project"],"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":"a9c4e251d3cd4cc41b93f5c3d6b1f081d4b05e1c4d97acfdc3bb3abca1d2f165","dependsOn":["build_contract_project"],"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":"05147ab5551623fa615443ff01cc8aa4aa28154677cc568eb9fbebe30049ce75","dependsOn":["build_contract_project","write_foundry_tests","manifest","audit_math","audit_permissions","audit_economics","audit_flow"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"af3aa01159bbf354f621cafa5c0006f6169e0938a8b015be508b7377dbf165bc","skillId":"audit-judge","tools":[]},"key":"audit_judge","kind":"code","role":"review","skillHash":"af3aa01159bbf354f621cafa5c0006f6169e0938a8b015be508b7377dbf165bc","skillId":"audit-judge","state":"waiting"},{"acceptedSubmissionHash":"7f4b567b9c3f67562b5b8abd2d007bf56d646150e8683c3cda035e1b435d803f","dependsOn":["build_contract_project"],"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":"46fa8ec563209a195a8ee5aa98bb3a53cff77bca07f8ff54bbe4a16377c026a4","dependsOn":["build_contract_project"],"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"},{"acceptedSubmissionHash":"287b5d74a8c599d4b1466ea367560b6b5ef11a09dc6637f6d85003fb1d5f74bd","dependsOn":[],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"d59561da3b378461b7c749f6bd5084477d44993aa94972ddea90f542535751df","skillId":"build-contract-project","tools":[]},"key":"build_contract_project","kind":"code","role":"implement","skillHash":"d59561da3b378461b7c749f6bd5084477d44993aa94972ddea90f542535751df","skillId":"build-contract-project","state":"failed"},{"acceptedSubmissionHash":"d1eb1547984393eb37b228a711715d0db69f53d9af985ef11fb4c3dc96614978","dependsOn":["build_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"tools":[]},"key":"manifest","kind":"code","role":"integrate","skillHash":null,"skillId":null,"state":"waiting"},{"acceptedSubmissionHash":"47040b3a50a594abbd97356664e4edb4798507246d4cd033698d08b266e86a21","dependsOn":["build_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"4c76766b00591dfe982fe85d3de589fdb2f810e5137c301155d1c5e36a1d3b23","skillId":"write-foundry-tests","tools":[]},"key":"write_foundry_tests","kind":"code","role":"tests","skillHash":"4c76766b00591dfe982fe85d3de589fdb2f810e5137c301155d1c5e36a1d3b23","skillId":"write-foundry-tests","state":"waiting"}],"objective":"A custom token: Mutually Assured Destruction (MAD).\nToken name: Mutually Assured Destruction\nToken symbol: MAD\nToken supply: 1,000,000,000 with 18 decimals, all minted once to the deployer in the constructor.\nWhat it does: 3% buy/sell tax that goes to 0x1EE3ffadfE138415A2ddaedeEBe5F46616fA413d\n\nWho receives fees and payouts: 0x1EE3ffadfE138415A2ddaedeEBe5F46616fA413d","parentJobId":null,"planHash":"279ab65015b222eb73fae896491801295e6611f446d5bf189e7495201189fc96","previousHash":"0000000000000000000000000000000000000000000000000000000000000000","projectId":"c722896c-4bf7-4a77-9548-3dc4b6aaf0e7","publication":{"commit":null,"deliveredAt":null,"repoUrl":"https://github.com/identity-md-launches/launch-2482-mutually-assured-destruction"},"receiptIdentity":{"adapter":"0xde152afb7db5373f34876e1499fbd893a82dd336","chainId":1,"collection":"0x0000ec93127baa929e58e97dd0095a2bfb38ec1d","registry":"0x8004a169fb4a3325136eb29fa0ceb6d2e539a432"},"registry":"0xb6d0a187b050fa5bb0b87033a203f37becf4a775","research":[],"schema":"identitymd-work-v1","signals":[{"agentId":"51530","feedbackHash":"ba20af917aa692c84d120d9182b9a2e5f06b203d6985616666c46ad843cb6595","nodeKey":"audit_economics","submissionHash":"45ad2de25f25302a3abf16dfdf71f7d95b14cdfabf27379b967b6d4371109e52","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51082","feedbackHash":"755603a4b8effe0e9cb8edec0d5f2df279af9e732c5c3293eef66b0b55f52d73","nodeKey":"audit_flow","submissionHash":"a9c4e251d3cd4cc41b93f5c3d6b1f081d4b05e1c4d97acfdc3bb3abca1d2f165","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52192","feedbackHash":"08bbb25cc3e04fa0a9f03f24360760db9deecfc0430dadaedc3903695404a1ef","nodeKey":"audit_judge","submissionHash":"05147ab5551623fa615443ff01cc8aa4aa28154677cc568eb9fbebe30049ce75","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51304","feedbackHash":"eeadee95e7d8cc81437b74f4836dcf21a3b83fae960afc70a0c9762455bda438","nodeKey":"audit_math","submissionHash":"7f4b567b9c3f67562b5b8abd2d007bf56d646150e8683c3cda035e1b435d803f","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51488","feedbackHash":"ecdd2f6d8a4d82779fb996fa2419b6eda1715a8fd3084ec06c5f25cdbecfce14","nodeKey":"audit_permissions","submissionHash":"46fa8ec563209a195a8ee5aa98bb3a53cff77bca07f8ff54bbe4a16377c026a4","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51877","feedbackHash":"6c0e991e07134017d2740f229524bdb84b5766199e1e6ca1e59ce7bcfe987bcb","nodeKey":"build_contract_project","submissionHash":"287b5d74a8c599d4b1466ea367560b6b5ef11a09dc6637f6d85003fb1d5f74bd","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"52207","feedbackHash":"74a5b119e9d81abb362ff61aa42757adb4739c8d24e59a81d9f6c1f3714ba864","nodeKey":"manifest","submissionHash":"d1eb1547984393eb37b228a711715d0db69f53d9af985ef11fb4c3dc96614978","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"51566","feedbackHash":"5bc7b4578dd0af605ec12a0bc40cc5dffab04e2197f7c399a111dbb1d5d2a12f","nodeKey":"write_foundry_tests","submissionHash":"47040b3a50a594abbd97356664e4edb4798507246d4cd033698d08b266e86a21","tag1":"verification:checks","tag2":"acceptance-v2","value":1}],"site":null,"snapshotHash":"9be2d96f69bbffaf70fd218a0b93b0a1a0807bab7da7c71e255e80ceb9598d04","state":"blocked","submissions":[{"artifacts":[],"attempt":1,"bundleHash":null,"device":"a7b8747ac077deb9","findings":[{"citation":"resolved","description":"The requester specified a 3% buy/sell tax paid to 0x1EE3ffadfE138415A2ddaedeEBe5F46616fA413d. _update splits only transfers whose sender is the PoolManager (line 83). A transfer whose recipient is the PoolManager (a sell) falls through to the plain OpenZeppelin update at line 92 and pays nothing, so a buy+sell round trip carries 3% instead of the 6% the brief implies, and a holder who received MAD untaxed (distributor claim, remainder allocation, wallet-to-wallet) sells into the pool paying nothing at all. The README discloses this and its reasoning is correct for a token-level tax: Uniswap v4 credits a sell by the PoolManager's balance change between sync and settle, so a token that deducts 3% from a transfer into the PoolManager leaves the swap 3% unsettled and the launch floor's trader (Token.protected.t.sol test_theSeedAndASwapEachWaySucceed: swap exact input, then settle) reverts with CurrencyNotSettled; charging the 3% on top is refused by the same floor test because it sells the trader's whole bought balance; a tax taken only from a seller's remaining balance is avoided by selling everything. So this is not a coding slip but an unmet requirement that needs the requester's explicit decision rather than a README note: (a) accept buy-only at 3% as built, (b) set TAX_BPS to 600 on the buy leg so the round trip carries the intended 6% (one-constant change, still passes the floor), or (c) move the sell-side tax to a venue the token can observe (a fee-taking v4 hook), which is a different launch kind. Merged from write_foundry_tests, audit_permissions, audit_economics, audit_math and audit_flow, which all reported the same root cause. Reproduced with the attached proof (two tests, both fail on this code with 0 != 30000000000000000000) and against the real PoolManager with .imd/reads/proofs/Proof_0d1a628adc2c.t.sol (fails: a 1,000 MAD sell paid no sell tax). The tree's own tests test/MADToken.t.sol test_sellToPoolArrivesWhole and test/MADTokenV4.t.sol:158 test_sellOfWholeBoughtBalanceSettlesAndIsUntaxed assert the 0 as intended.","line":92,"path":"src/MADToken.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {MADToken} from \"src/MADToken.sol\";\n\n/// @notice Finding: the brief specifies a 3% buy/sell tax to the tax wallet. A sell (a transfer\n/// whose recipient is the PoolManager) pays nothing. Expected after selling 1000 MAD: the tax\n/// wallet gained 30 MAD. Actual: 0.\ncontract SellTaxMissingTest is Test {\n    address constant TAX_WALLET = 0x1EE3ffadfE138415A2ddaedeEBe5F46616fA413d;\n    address constant FACTORY = address(0xFAC7);\n    address constant POOL_MANAGER = address(0x900C);\n    address constant ALICE = address(0xA11CE);\n\n    MADToken token;\n\n    function setUp() public {\n        vm.prank(FACTORY);\n        token = new MADToken(FACTORY, POOL_MANAGER);\n        vm.prank(FACTORY);\n        token.transfer(ALICE, 1_000e18);\n    }\n\n    function test_sellPaysThreePercentToTaxWallet() public {\n        uint256 taxBefore = token.balanceOf(TAX_WALLET);\n        vm.prank(ALICE);\n        token.transfer(POOL_MANAGER, 1_000e18);\n        assertEq(token.balanceOf(TAX_WALLET) - taxBefore, 30e18, \"a 1000 MAD sell paid no 3% tax\");\n    }\n\n    function test_sellViaTransferFromPaysThreePercentToTaxWallet() public {\n        address router = address(0x5007);\n        vm.prank(ALICE);\n        token.approve(router, 1_000e18);\n        uint256 taxBefore = token.balanceOf(TAX_WALLET);\n        vm.prank(router);\n        token.transferFrom(ALICE, POOL_MANAGER, 1_000e18);\n        assertEq(token.balanceOf(TAX_WALLET) - taxBefore, 30e18, \"a 1000 MAD sell paid no 3% tax\");\n    }\n}","reproduction":"State: vm.prank(FACTORY=0xFAC7); token = new MADToken(FACTORY, POOL_MANAGER=0x900C); FACTORY transfers 1000e18 MAD to ALICE (plain transfer, no tax). Call: vm.prank(ALICE); token.transfer(POOL_MANAGER, 1000e18). Expected under the brief (3% buy/sell tax): balanceOf(TAX_WALLET) == 30e18 and the PoolManager receives 970e18 (or 1000e18 with 30e18 charged elsewhere). Actual: balanceOf(TAX_WALLET) == 0, balanceOf(POOL_MANAGER) == 1000e18, no BuyTaxed event. Same via an approved router: ALICE approves 0x5007 for 1000e18, 0x5007 calls transferFrom(ALICE, POOL_MANAGER, 1000e18): tax wallet still 0. Run: forge test --match-path test/scratch/Proof_9b2a28a1eb79.t.sol (both tests fail: 0 != 30000000000000000000).","severity":"medium","snippet":"        super._update(from, to, value);","title":"Sell leg is untaxed: the brief's 3% buy/sell tax is delivered as a 3% buy-only tax"},{"citation":"resolved","description":"The only tax the token charges is on an ERC-20 transfer whose sender is the PoolManager. Uniswap v4 does not require a buyer to take the output as an ERC-20: inside the same unlock the trader may call PoolManager.mint(to, currency.toId(), amount) and keep the bought MAD as an ERC-6909 claim balance inside the PoolManager; MAD's ERC-20 balances never move, _update never runs with from == POOL_MANAGER, no BuyTaxed event is emitted and the tax wallet receives nothing. A later sell is paid with PoolManager.burn(from, id, amount), again with no ERC-20 transfer, and claims are freely transferable between accounts with the PoolManager's ERC-6909 transfer/transferFrom. Multi-hop v4 routes where MAD is an intermediate currency never leave the PoolManager either. So any unprivileged caller with a custom router (every arbitrage or MEV bot) can buy, hold, transfer and sell MAD through the launch pool paying 0% in both directions, while an ordinary wallet using take pays 3%; a liquidity provider can likewise remove the MAD leg of a position as claims untaxed. The 3% is only paid by whoever finally converts a claim back to ERC-20 MAD. The README and the NatSpec (lines 15-18) do not mention this path. The token cannot observe ERC-6909 accounting, so there is no fix inside _update; closing it needs a tax charged on the swap delta (a v4 hook), which is a different launch kind, and the launch pool's hook slot is taken by the factory's initialization guard. This is a scope decision for the requester: accept an avoidable buy tax and document that it applies only to ERC-20 withdrawals from the PoolManager, or change the fee design. Severity is medium, not high: the party shorted is the tax wallet's fee revenue, no holder's funds are lost and nothing is paid to a wrong party, and it only arises for traders who use claim settlement. Merged from write_foundry_tests, audit_permissions (reported high), audit_economics, audit_math and audit_flow. Note: the audit_math claim that the Universal Router exposes claim settlement as a flag was not verified; the bypass needs a custom unlock callback, which the proof supplies.","line":83,"path":"src/MADToken.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {ERC20} from \"@openzeppelin/contracts/token/ERC20/ERC20.sol\";\nimport {IERC20} from \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\nimport {PoolManager} from \"v4-core/src/PoolManager.sol\";\nimport {IPoolManager} from \"v4-core/src/interfaces/IPoolManager.sol\";\nimport {IUnlockCallback} from \"v4-core/src/interfaces/callback/IUnlockCallback.sol\";\nimport {IHooks} from \"v4-core/src/interfaces/IHooks.sol\";\nimport {PoolKey} from \"v4-core/src/types/PoolKey.sol\";\nimport {Currency, CurrencyLibrary} from \"v4-core/src/types/Currency.sol\";\nimport {BalanceDelta} from \"v4-core/src/types/BalanceDelta.sol\";\nimport {ModifyLiquidityParams, SwapParams} from \"v4-core/src/types/PoolOperation.sol\";\nimport {TickMath} from \"v4-core/src/libraries/TickMath.sol\";\nimport {MADToken} from \"src/MADToken.sol\";\n\n/// @notice The paired currency: a plain 18-decimal ERC-20 standing in for FWA.\ncontract PairToken is ERC20 {\n    constructor() ERC20(\"Pair\", \"PAIR\") {}\n\n    function mint(address to, uint256 amount) external {\n        _mint(to, amount);\n    }\n}\n\n/// @notice Seeds a two-sided position the ordinary way (sync / transfer / settle for both legs).\ncontract LiquidityRouter is IUnlockCallback {\n    IPoolManager immutable manager;\n\n    constructor(IPoolManager manager_) {\n        manager = manager_;\n    }\n\n    function modify(PoolKey calldata key, ModifyLiquidityParams calldata params) external returns (BalanceDelta) {\n        return abi.decode(manager.unlock(abi.encode(key, params)), (BalanceDelta));\n    }\n\n    function unlockCallback(bytes calldata data) external returns (bytes memory) {\n        require(msg.sender == address(manager), \"not the pool manager\");\n        (PoolKey memory key, ModifyLiquidityParams memory params) = abi.decode(data, (PoolKey, ModifyLiquidityParams));\n        (BalanceDelta delta,) = manager.modifyLiquidity(key, params, \"\");\n        _pay(key.currency0, delta.amount0());\n        _pay(key.currency1, delta.amount1());\n        return abi.encode(delta);\n    }\n\n    function _pay(Currency currency, int128 delta) internal {\n        require(delta <= 0, \"seed only pays\");\n        if (delta < 0) {\n            manager.sync(currency);\n            IERC20(Currency.unwrap(currency)).transfer(address(manager), uint256(uint128(-delta)));\n            manager.settle();\n        }\n    }\n}\n\n/// @notice A trader that keeps its MAD inside the PoolManager as ERC-6909 claims. Buying mints\n/// claims instead of taking the ERC-20 out; selling burns claims instead of transferring in.\n/// Neither step moves MAD through `MADToken._update`, so the 3% buy tax is never charged.\ncontract ClaimsTrader is IUnlockCallback {\n    using CurrencyLibrary for Currency;\n\n    IPoolManager immutable manager;\n    PoolKey key;\n    Currency mad;\n\n    constructor(IPoolManager manager_, PoolKey memory key_, Currency mad_) {\n        manager = manager_;\n        key = key_;\n        mad = mad_;\n    }\n\n    function swap(bool zeroForOne, int256 amountSpecified) external returns (BalanceDelta) {\n        return abi.decode(manager.unlock(abi.encode(zeroForOne, amountSpecified)), (BalanceDelta));\n    }\n\n    function unlockCallback(bytes calldata data) external returns (bytes memory) {\n        require(msg.sender == address(manager), \"not the pool manager\");\n        (bool zeroForOne, int256 amountSpecified) = abi.decode(data, (bool, int256));\n        uint160 limit = zeroForOne ? TickMath.MIN_SQRT_PRICE + 1 : TickMath.MAX_SQRT_PRICE - 1;\n        BalanceDelta delta = manager.swap(key, SwapParams(zeroForOne, amountSpecified, limit), \"\");\n        _settle(key.currency0, delta.amount0());\n        _settle(key.currency1, delta.amount1());\n        return abi.encode(delta);\n    }\n\n    function _settle(Currency currency, int128 delta) internal {\n        if (delta == 0) return;\n        bool isMad = Currency.unwrap(currency) == Currency.unwrap(mad);\n        if (delta > 0) {\n            uint256 amount = uint256(uint128(delta));\n            if (isMad) {\n                // Buy: keep the MAD inside the PoolManager as a claim. No ERC-20 transfer happens.\n                manager.mint(address(this), currency.toId(), amount);\n            } else {\n                manager.take(currency, address(this), amount);\n            }\n        } else {\n            uint256 amount = uint256(uint128(-delta));\n            if (isMad) {\n                // Sell: pay with claims. No ERC-20 transfer happens.\n                manager.burn(address(this), currency.toId(), amount);\n            } else {\n                manager.sync(currency);\n                IERC20(Currency.unwrap(currency)).transfer(address(manager), amount);\n                manager.settle();\n            }\n        }\n    }\n}\n\n/// @notice Fails on the current MADToken: a buy settled as ERC-6909 claims pays no tax, and the\n/// claims can be sold back (burned) or transferred without ever paying it either.\ncontract MADClaimsBypassTest is Test {\n    using CurrencyLibrary for Currency;\n\n    address constant TAX_WALLET = 0x1EE3ffadfE138415A2ddaedeEBe5F46616fA413d;\n    uint24 constant POOL_FEE = 12_500;\n    int24 constant TICK_SPACING = 60;\n    uint160 constant ONE_TO_ONE = 79_228_162_514_264_337_593_543_950_336;\n\n    PoolManager manager;\n    MADToken token;\n    PairToken pair;\n    LiquidityRouter lp;\n    ClaimsTrader trader;\n    PoolKey key;\n    bool tokenIsZero;\n\n    function setUp() public {\n        manager = new PoolManager(address(this));\n        pair = new PairToken();\n        // This test contract plays the factory: it deploys the token and holds the supply.\n        token = new MADToken(address(this), address(manager));\n        lp = new LiquidityRouter(manager);\n\n        tokenIsZero = address(token) < address(pair);\n        (Currency c0, Currency c1) = tokenIsZero\n            ? (Currency.wrap(address(token)), Currency.wrap(address(pair)))\n            : (Currency.wrap(address(pair)), Currency.wrap(address(token)));\n        key = PoolKey(c0, c1, POOL_FEE, TICK_SPACING, IHooks(address(0)));\n        manager.initialize(key, ONE_TO_ONE);\n\n        token.transfer(address(lp), 10_000_000e18);\n        pair.mint(address(lp), 10_000_000e18);\n        lp.modify(key, ModifyLiquidityParams(-600, 600, 1_000_000e18, bytes32(0)));\n\n        trader = new ClaimsTrader(manager, key, Currency.wrap(address(token)));\n    }\n\n    function _tokenDelta(BalanceDelta delta) internal view returns (int128) {\n        return tokenIsZero ? delta.amount0() : delta.amount1();\n    }\n\n    function test_buyAsClaimsPaysNoTax() public {\n        pair.mint(address(trader), 1_000e18);\n        uint256 poolBefore = token.balanceOf(address(manager));\n        uint256 madId = Currency.wrap(address(token)).toId();\n\n        // Buy: 100 PAIR in, MAD out, kept as ERC-6909 claims.\n        BalanceDelta delta = trader.swap(!tokenIsZero, -100e18);\n        uint256 gross = uint256(uint128(_tokenDelta(delta)));\n        assertGt(gross, 0, \"no MAD came out of the swap\");\n        assertEq(manager.balanceOf(address(trader), madId), gross, \"trader did not get full gross as claims\");\n        assertEq(token.balanceOf(address(manager)), poolBefore, \"no ERC-20 left the PoolManager\");\n\n        // Sell everything back by burning the claims: still no ERC-20 transfer.\n        uint256 pairBefore = pair.balanceOf(address(trader));\n        trader.swap(tokenIsZero, -int256(gross));\n        assertEq(manager.balanceOf(address(trader), madId), 0, \"claims not fully burned\");\n        assertGt(pair.balanceOf(address(trader)), pairBefore, \"no PAIR came back from the sell\");\n\n        // The contract's stated guarantee: every buy from the pool pays 3% of the gross to the tax wallet.\n        // Here a full buy-then-sell round trip through the pool paid nothing.\n        uint256 expectedTax = (gross * token.TAX_BPS()) / token.BPS_DENOMINATOR();\n        assertEq(token.balanceOf(TAX_WALLET), expectedTax, \"buy settled as ERC-6909 claims paid no buy tax\");\n    }\n}","reproduction":"State: real v4 PoolManager (new PoolManager(this)), MADToken(factory=this, manager), MAD/PAIR pool at sqrtPrice 2^96 (1:1), fee 12500, tick spacing 60, no hook, LiquidityRouter adds 1_000_000e18 liquidity in [-600, 600] paying both legs by sync/transfer/settle. ClaimsTrader holds 1000e18 PAIR. Call 1 (inside unlock): swap(PAIR->MAD, exact input 100e18) returns a MAD delta of 98740249400371713293; settle PAIR by sync/transfer/settle; settle the MAD delta with manager.mint(trader, Currency.wrap(MAD).toId(), 98740249400371713293). Actual: manager.balanceOf(trader, madId) == 98740249400371713293, token.balanceOf(manager) unchanged, token.balanceOf(TAX_WALLET) == 0. Expected under the README's own guarantee ('Buy: 97% to trader, 3% to tax wallet'): TAX_WALLET += 2962207482011151398. Call 2: swap(MAD->PAIR, exact input 98740249400371713293) settled with manager.burn(trader, madId, amount), PAIR taken: trader's PAIR balance rises, claims are 0, TAX_WALLET still 0. Compare test/MADTokenV4.t.sol:142 test_buyPaysThreePercentOfTheGrossOutput where the identical swap settled with take pays 2962207482011151398. Run: forge test --match-path test/scratch/Proof_31bc87a4bcfb.t.sol (fails: 'buy settled as ERC-6909 claims paid no buy tax: 0 != 2962207482011151398'). Exact-output variant: .imd/reads/proofs/Proof_6c22e85c2425.t.sol fails with 0 != 3000000000000000000.","severity":"medium","snippet":"        if (from == POOL_MANAGER && !isTaxExempt(to)) {","title":"Buy tax is bypassed by settling the MAD swap output as Uniswap v4 ERC-6909 claims inside the PoolManager"},{"citation":"resolved","description":"'Sender is the PoolManager' is used as the definition of a buy, but the PoolManager also pays out the MAD leg of modifyLiquidity with a negative liquidityDelta, accrued MAD swap fees collected by a position owner (including an add to an existing position whose accrued fees exceed the deposit, where the net delta pays out), and a settlement surplus taken back, all through take(). Each of these is charged 3% to the tax wallet although no purchase occurred, while adding liquidity is untaxed, so an outside LP that deposits X MAD and withdraws it receives 0.97X. Combined with finding 2, an LP settling with mint (claims) pays nothing, so the cost falls only on LPs using standard tooling (PositionManager TAKE_PAIR). The launch's own seeded position is unaffected only while it is withdrawn to an exempt address (the factory); if the factory ever withdraws the 800,000,000 MAD seed position to a non-exempt address such as the requester's wallet, 3% of the remaining MAD leg goes to the tax wallet. The README discloses the LP case under 'Side effects to be aware of' and the tree's test test/MADTokenV4.t.sol:183 test_removingLiquidityIsTreatedAsABuyOfTheTokenLeg asserts it as intended. No token-level fix exists without a venue signal (the token cannot tell a take() after swap from a take() after modifyLiquidity); reported low so the requester confirms that third-party LPs forfeiting 3% of their own principal is accepted. Merged from write_foundry_tests, audit_permissions, audit_economics, audit_math and audit_flow.","line":83,"path":"src/MADToken.sol","reproduction":"State: test/MADTokenV4.t.sol setUp (real PoolManager, MAD/PAIR at 1:1, LiquidityRouter lp holds a position of liquidity 1_000_000e18 in [-600, 600], TAX_WALLET balance 0). Call: lp.modify(key, ModifyLiquidityParams(-600, 600, -1_000_000e18, bytes32(0))), which takes both legs to lp. Let G be the positive MAD leg of the returned BalanceDelta. Expected for a withdrawal of one's own liquidity: lp's MAD balance rises by G and the tax wallet is unchanged. Actual: lp's MAD balance rises by G - G*300/10000 and balanceOf(TAX_WALLET) == G*300/10000. This is exactly what the passing tree test test_removingLiquidityIsTreatedAsABuyOfTheTokenLeg asserts (run with FOUNDRY_FUZZ_SEED as in the README; 60 tests pass). audit_flow measured a concrete instance: deposit of 2955301087913716968083 MAD, withdrawal delivered 2866642055276305459040 to the LP and 88659032637411509042 to the tax wallet.","severity":"low","snippet":"        if (from == POOL_MANAGER && !isTaxExempt(to)) {","title":"Every ERC-20 exit from the PoolManager is taxed as a buy, including liquidity withdrawal and fee collection"},{"citation":"resolved","description":"_taxOf truncates toward the buyer: a gross of 1..33 wei pays 0 tax (33*300/10000 == 0) and every buy loses the fractional wei, so split buys always pay at most what one buy of the same total pays. The auditor checklist prefers fees that round toward the fee recipient. Impact is dust at 18 decimals (33 wei of MAD is far below swap gas), there is no compounding, and no overflow is reachable in _update (gross <= 1e27). previewBuy(buyer, g) reverts with Panic(0x11) for g > type(uint256).max / 300 instead of returning, which is harmless for a view. The README states the 34-wei threshold. Minimal change if the fee-favouring direction is wanted: tax = (grossAmount * TAX_BPS + BPS_DENOMINATOR - 1) / BPS_DENOMINATOR, and update the README sentence. From audit_math; kept as information, not a defect that reopens the work.","line":96,"path":"src/MADToken.sol","reproduction":"State: FACTORY=0xFAC7 deploys the token and transfers the supply to POOL_MANAGER=0x900C. Calls from POOL_MANAGER: transfer(ALICE, 33) repeated 100 times -> ALICE 3300, TAX_WALLET 0, no BuyTaxed event; transfer(BOB, 3300) -> BOB 3201, TAX_WALLET 99; transfer(ALICE, 66) -> tax 1 (true 1.98), ALICE +65. previewBuy(ALICE, type(uint256).max) reverts with arithmetic overflow. Expected under round-up fees: 33 wei -> 1 wei tax, 66 wei -> 2 wei tax.","severity":"info","snippet":"        return (grossAmount * TAX_BPS) / BPS_DENOMINATOR;","title":"Tax rounds down: buys below 34 wei pay nothing and each buy under-collects by up to 1 wei"},{"citation":"resolved","description":"v4 routers enforce amountOutMinimum against the swap's BalanceDelta, which is the gross amount debited from the PoolManager; the recipient's actual credit is value - tax. A buyer who sets amountOutMinimum = X can receive 0.97X without the router reverting, and an exact-output swap for X delivers 0.97X. The README discloses this ('Routers that check output on the swap delta see the gross amount') and it is inherent to a transfer-level tax; there is no on-chain fix in the token. Recorded so front-ends quote MAD buys net of 3%. From audit_math.","line":89,"path":"src/MADToken.sol","reproduction":"State: test/MADTokenV4.t.sol setUp (real PoolManager, MAD/PAIR at 1:1). Call: trader.swap(key, !tokenIsZero, 100e18) (exact output of 100e18 MAD, settled with take). Actual: the MAD leg of the BalanceDelta is +100e18 (what a router compares against amountOutMinimum), token.balanceOf(trader) == 97e18, balanceOf(TAX_WALLET) == 3e18. Expected by a router's slippage check: the buyer holds 100e18. Asserted by the passing tree test test/MADTokenV4.t.sol:175 test_exactOutputBuyDeliversNetOfTax.","severity":"info","snippet":"            super._update(from, to, value - tax);","title":"Routers' minimum-output checks and exact-output swaps see the gross amount; the buyer receives 3% less"}],"hash":"05147ab5551623fa615443ff01cc8aa4aa28154677cc568eb9fbebe30049ce75","nodeId":"432d3970-727b-4c10-95a3-0e34e15ddab3","outcome":"completed","summary":"The review is complete and `.imd-findings.json` holds the result. No repository file outside test/scratch was changed, and test/scratch is ignored and not submitted.\n\n**Outcome.** The token has no coding defect in its ERC-20 surface, but the delivered tax rule differs from the brief in two ways that need the requester's decision. Both reproduce with failing Foundry tests.\n\n**Findings kept, merged from the five specialist reports:**\n\n1. **Medium: sells are untaxed.** The brief asks for a 3% buy/sell tax. A transfer into the PoolManager falls through to the plain update at line 92 of src/MADToken.sol and pays nothing. The README's reason is correct on Uniswap v4, since a deducted sell tax leaves the swap unsettled and the launch floor sells the trader's whole balance. It remains an unmet requirement the requester must settle explicitly, either by accepting buy-only, raising the buy rate to 600 bps, or moving the sell tax into a hook. Proof attached: 1,000 MAD sold to the PoolManager pays 0 instead of 30 MAD, via both transfer and transferFrom.\n2. **Medium: the buy tax is bypassed with ERC-6909 claims.** A trader who settles the swap output with PoolManager.mint instead of take never moves ERC-20 MAD, so the override at line 83 never runs. Claims can be burned to fund a later sell and transferred between wallets, so a full round trip pays zero. Proof attached against the real PoolManager: a 100 PAIR buy settled as claims pays 0 instead of about 2.96 MAD. I set this at medium rather than the permissions specialist's high, since only fee revenue is lost and no holder's funds move to a wrong party.\n3. **Low: every ERC-20 exit from the PoolManager is taxed as a buy,** including liquidity withdrawal and fee collection. Disclosed in the README and asserted by the tree's own test, but value is taken from a party the brief does not name.\n4. **Info: the tax rounds down,** so buys under 34 wei pay nothing. Dust only.\n5. **Info: routers' minimum-output checks and exact-output swaps see the gross amount,** so the buyer receives 3% less than the delta a router protects.\n\n**Dropped or merged.** Each of the five areas reported the same sell-tax and claims-bypass root causes, so they collapsed into findings 1 and 2. The math specialist's claim that the Universal Router exposes claim settlement as a flag was not verified and is noted as such. Nothing failed to reproduce.\n\n**Coverage.** All three entry points are answered: approve holds, transfer and transferFrom carry finding 1. Five extra rows record the constructor and supply, the launch flows, the taxed buy path, the absence of privileged functions, and the manifest's shape, all of which hold. The project's own 60 tests pass, and all four specialist proofs fail on this code for the reasons stated.","treeHash":null,"usage":{"cachedInputTokens":817167,"inputTokens":290,"model":"claude-fable-5-1","outputTokens":21333,"runtime":"claude","turns":22,"wallClockMs":297848}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"1188f01303e45ecd","findings":[],"hash":"21adb28c5230936a6f6c8f94dcbe9eb938d69cbb6c25d523c5e375d14b58e632","nodeId":"c54fa86f-dd25-431b-9f92-8ef7630ac989","outcome":"failed","summary":"needs input: A decision on the tax design. The brief asks for a 3% buy/sell tax, but on this Uniswap v4 launch an ERC-20 token can only tax ERC-20 withdrawals of MAD from the PoolManager: a sell cannot be taxed without breaking the launch's admission swap, and a buy kept inside the PoolManager as ERC-6909 claims never calls the token. The independent review reopened the accepted build for this and asked for the requester's explicit choice. The effective rate is a tax rate, which is the requester's to set, not ours. — The token can tax only ERC-20 withdrawals of MAD from the Uniswap v4 PoolManager. A sell cannot be taxed: the launch's admission check sells a trader's whole balance with swap-then-settle, and any amount shaved from a transfer into the PoolManager leaves the swap unsettled and reverts (verified against the real PoolManager: CurrencyNotSettled). A buy kept inside the PoolManager as ERC-6909 claims never calls the token, so it is untaxed until the holder withdraws it as ERC-20. Which do ","treeHash":null,"usage":{"cachedInputTokens":1098760,"inputTokens":386,"model":"claude-fable-5-1","outputTokens":29763,"runtime":"claude","turns":21,"wallClockMs":455825}},{"artifacts":[],"attempt":1,"bundleHash":"8aa04f278d6ff1e2786dfe4c23f69c264b12d078cecd524eedb56c3db98af114","device":"f494611affb5524c","findings":[],"hash":"287b5d74a8c599d4b1466ea367560b6b5ef11a09dc6637f6d85003fb1d5f74bd","nodeId":"c54fa86f-dd25-431b-9f92-8ef7630ac989","outcome":"completed","summary":"Done. The MAD token is built, tested and committed on main.\n\n**What was delivered**\n\n- `src/MADToken.sol`: contract `MADToken`, name \"Mutually Assured Destruction\", symbol MAD, 18 decimals, 1,000,000,000 tokens minted once to the deployer (the factory). Constructor takes `$factory` and `$poolManager`. No owner, no mint, no pause, no blacklist, no setters.\n- Buy tax: any transfer out of the Uniswap v4 PoolManager to a non-exempt address sends 3% to `0x1EE3ffadfE138415A2ddaedeEBe5F46616fA413d` and 97% to the buyer. The pool is debited the gross amount, so v4 accounting stays whole.\n- Launch flows (factory to distributor, distributor to claimant, factory to remainder wallet, seed into the pool) move exactly what they say.\n- Tests: 36 passing. A unit suite covers supply, events, rounding, atomic revert, exemptions and fuzzed conservation. An integration suite runs against the real v4 PoolManager: untaxed seed, exact-input and exact-output buys taxed 3%, a sell of the whole bought balance that settles, liquidity removal, and a fuzzed buy-then-sell-all round trip.\n- Dependencies vendored as plain files, no submodules: OpenZeppelin v5.1.0, forge-std v1.9.7, Uniswap v4-core and solmate's `Owned`.\n- README documents parameters, manifest constructor arguments, trust assumptions, operational responsibilities and side effects.\n\n**Checks run**\n\n```\nforge build            ok (solc 0.8.26, cancun, bytecode_hash = none)\nforge test             36 passed, 0 failed (with the verifier's FOUNDRY_FUZZ_SEED)\nforge fmt --check      ok\n```\n\n**One deviation the requester should know about**\n\nThe brief asks for a 3% buy and sell tax. The sell leg is untaxed on the v4 pool, and this is a hard constraint rather than a choice. Uniswap v4 credits a sell by measuring the PoolManager's balance change, so a token that deducts 3% on the way in leaves the swap unsettled and the sale reverts. The launch admission test sells a trader's entire bought balance with that exact flow, which also rules out charging the tax on top. The README explains this in full. If the requester wants the round trip to carry 6%, the buy-side rate is theirs to set; this build keeps the 3% they specified. Trades on other venues such as Uniswap v2 or v3 are plain transfers and are untaxed too, since no pool registry exists and adding one would mean an owner.","treeHash":"0fb8a0db32c92221d0ac97823e956234b2cd948f","usage":{"cachedInputTokens":1162730,"inputTokens":450,"model":"claude-fable-5-1","outputTokens":46171,"runtime":"claude","turns":29,"wallClockMs":603201}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"5ded77c3c883b7b0","findings":[{"citation":"resolved","description":"The only tax the token charges is on an ERC-20 transfer whose sender is the PoolManager. Uniswap v4 does not require a buyer to take the output as an ERC-20: inside the same unlock a trader may call PoolManager.mint(to, currency.toId(), amount) and keep the bought MAD as an ERC-6909 claim balance inside the PoolManager (lib/v4-core/src/PoolManager.sol:322). Later it sells by paying the swap input with PoolManager.burn (line 332), and can hand exposure to anyone with the PoolManager's ERC-6909 transfer (lib/v4-core/src/ERC6909.sol:25). None of these moves MAD's ERC-20 balances, so _update never runs with from == POOL_MANAGER and the tax wallet 0x1EE3ffadfE138415A2ddaedeEBe5F46616fA413d receives nothing. The same holds for multi-hop v4 routes where MAD is an intermediate currency. Economic Security guide: 'legitimate features turned against the protocol' and 'push fee formulas to zero'. Invariant broken: 'every buy from the pool pays 3% of gross to TAX_WALLET'. The README states the tax is 'charged once per exit from the pool', but does not disclose that v4 lets a trader never exit. Who profits: any trader or bot that settles in claims saves 3% per buy; who loses: the tax wallet, whose revenue falls to zero for that flow. Cost to the trader: none beyond ordinary swap gas (mint is cheaper than an ERC-20 take). The token itself cannot observe claim balances, so a fix needs a scope decision: accept the limitation and document it, or move the tax into a v4 hook (beforeSwap/afterSwap) that charges on the swap delta regardless of settlement form. The latter is outside this build's shape (the launch pool's hook is the factory's initialization guard), so the requester must decide.","line":83,"path":"src/MADToken.sol","reproduction":"State: MAD/PAIR pool at 1:1 seeded two-sided (as in test/MADTokenV4.t.sol setUp). Trader holds 1000 PAIR. Calls: trader.unlock -> swap(exact input 100e18 PAIR for MAD) -> gross output G ~= 98.74e18 MAD (the test computes expected tax G*300/10000 = 2962207482011151398 wei) -> settle PAIR by sync/transfer/settle -> PoolManager.mint(trader, MAD.toId(), G). Expected: TAX_WALLET MAD balance increases by G*3/100. Actual: TAX_WALLET balance stays 0, trader holds G as ERC-6909 claims. Follow-up: trader.transfer(friend, id, G/2); friend swaps G/2 MAD->PAIR paying via burn; trader sells the rest via burn. Tax wallet still 0 after a buy, a transfer and two sells. Run: forge test --match-path test/scratch/ClaimsBypass.t.sol (both tests fail on this code).","severity":"medium","snippet":"        if (from == POOL_MANAGER && !isTaxExempt(to)) {","title":"Buy tax is bypassed entirely by holding and trading MAD as Uniswap v4 ERC-6909 claims"},{"citation":"resolved","description":"The requester specified a 3% buy/sell tax to 0x1EE3ffadfE138415A2ddaedeEBe5F46616fA413d. _update taxes only transfers out of the PoolManager; a transfer into it (a sell) falls through to the plain OpenZeppelin update on line 92 and pays nothing. The README (section 'Why sells are not taxed on the v4 pool') discloses this and its reasoning is sound: v4 credits a sell input by the PoolManager's measured balance change, so a deducted sell tax leaves the swap unsettled and reverts, and the launch floor (Token.protected.t.sol test_theSeedAndASwapEachWaySucceed) sells the trader's whole bought balance, which rules out charging it on top. This is reported so the judge and requester make the scope decision explicitly rather than inherit it from a README: the requester's revenue per round trip is 3% of the buy leg, not the 6% the brief implies, and a holder who received MAD untaxed (airdrop claim, OTC transfer, remainder allocation) can sell into the pool with no tax at all. Flow Gap seam: execution x first principles, the path completes cleanly and the end state contradicts the stated economics. Options that preserve the floor: raise TAX_BPS on the buy leg (e.g. 600) to recover the round-trip take, or accept buy-only and amend the brief. A sell tax capped at the seller's remaining balance would pass the floor but is avoidable by selling everything, so it is not recommended.","line":92,"path":"src/MADToken.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {MADToken} from \"src/MADToken.sol\";\n\n/// @notice Finding: the brief specifies a 3% buy/sell tax to the tax wallet. A sell (a transfer\n/// whose recipient is the PoolManager) pays nothing. Expected after selling 1000 MAD: the tax\n/// wallet gained 30 MAD. Actual: 0.\ncontract SellTaxMissingTest is Test {\n    address constant TAX_WALLET = 0x1EE3ffadfE138415A2ddaedeEBe5F46616fA413d;\n    address constant FACTORY = address(0xFAC7);\n    address constant POOL_MANAGER = address(0x900C);\n    address constant ALICE = address(0xA11CE);\n\n    MADToken token;\n\n    function setUp() public {\n        vm.prank(FACTORY);\n        token = new MADToken(FACTORY, POOL_MANAGER);\n        vm.prank(FACTORY);\n        token.transfer(ALICE, 1_000e18);\n    }\n\n    function test_sellPaysThreePercentToTaxWallet() public {\n        uint256 taxBefore = token.balanceOf(TAX_WALLET);\n        vm.prank(ALICE);\n        token.transfer(POOL_MANAGER, 1_000e18);\n        assertEq(token.balanceOf(TAX_WALLET) - taxBefore, 30e18, \"a 1000 MAD sell paid no 3% tax\");\n    }\n\n    function test_sellViaTransferFromPaysThreePercentToTaxWallet() public {\n        address router = address(0x5007);\n        vm.prank(ALICE);\n        token.approve(router, 1_000e18);\n        uint256 taxBefore = token.balanceOf(TAX_WALLET);\n        vm.prank(router);\n        token.transferFrom(ALICE, POOL_MANAGER, 1_000e18);\n        assertEq(token.balanceOf(TAX_WALLET) - taxBefore, 30e18, \"a 1000 MAD sell paid no 3% tax\");\n    }\n}","reproduction":"State: token deployed with FACTORY=0xFAC7, POOL_MANAGER=0x900C; FACTORY transfers 1000e18 MAD to ALICE. Call: vm.prank(ALICE); token.transfer(POOL_MANAGER, 1000e18). Expected per brief: TAX_WALLET gains 30e18, PoolManager gains 970e18 (or 1000e18 with 30e18 charged elsewhere). Actual: TAX_WALLET gains 0, PoolManager gains 1000e18. Same via transferFrom from an approved router. Run: forge test --match-path test/scratch/SellTaxMissing.t.sol (both tests fail on this code with '0 != 30000000000000000000').","severity":"medium","snippet":"        super._update(from, to, value);","title":"Sells are untaxed: the brief's 3% buy/sell tax is delivered as a 3% buy-only tax"},{"citation":"resolved","description":"Every ERC-20 transfer out of the PoolManager to a non-exempt address is treated as a buy. Removing liquidity or collecting accrued LP fees takes the MAD leg from the PoolManager, so a third-party LP that deposits X MAD and withdraws it receives 0.97 X, with 0.03 X paid to the tax wallet, although no purchase occurred. The requester's own seed position is unaffected because the factory is exempt, but any outside LP loses 3% of principal per deposit/withdraw cycle, which discourages anyone but the factory from providing MAD liquidity and so lowers depth for the trades that do pay tax. The README lists this under 'Side effects to be aware of' and the project's own test asserts it; it is reported because, from the economics brief, a tax on LP principal is money paid by a party the brief does not name. LPs can avoid it by taking the MAD leg as ERC-6909 claims (see finding 1), which also means the leak is uneven: sophisticated LPs pay nothing, ordinary PositionManager users pay 3%. Fix needs a scope decision: either accept (document for LPs) or exempt recognised position managers, which reintroduces an address list the design currently avoids.","line":83,"path":"src/MADToken.sol","reproduction":"State: test/MADTokenV4.t.sol setUp (LiquidityRouter lp adds 1_000_000e18 liquidity around 1:1 from its own 10_000_000e18 MAD and PAIR). Call: lp.modify(key, ModifyLiquidityParams(-600, 600, -1_000_000e18, 0)) to remove the whole position. Expected for a non-trade: lp's MAD balance rises by the gross MAD delta G. Actual: it rises by G - G*300/10000 and TAX_WALLET gains G*300/10000. This is exactly what the project's test_removingLiquidityIsTreatedAsABuyOfTheTokenLeg (test/MADTokenV4.t.sol:183-190) asserts and it passes.","severity":"low","snippet":"        if (from == POOL_MANAGER && !isTaxExempt(to)) {","title":"Liquidity providers pay the 3% buy tax on withdrawing their own MAD principal and on collecting LP fees"}],"hash":"45ad2de25f25302a3abf16dfdf71f7d95b14cdfabf27379b967b6d4371109e52","nodeId":"a49c35e4-d44b-4af1-bb56-bad256b319fb","outcome":"completed","summary":"Review complete. No tracked files were changed; the findings are in `.imd-findings.json` with proofs embedded, and the scratch tests live under `test/scratch/`.\n\n**Findings (3)**\n\n1. **Medium: the buy tax is fully avoidable with Uniswap v4 ERC-6909 claims.** The tax fires only on an ERC-20 transfer out of the PoolManager. A trader can swap and then call the PoolManager's `mint` to keep the bought MAD as a claim balance, sell later by paying with `burn`, and pass exposure to others with the PoolManager's ERC-6909 transfer. None of that touches MAD's ERC-20 balances, so the tax wallet receives nothing. Reproduced against the real v4 PoolManager: a 100 PAIR buy that should have paid about 2.96 MAD of tax paid zero, and a follow-up transfer plus two sells also paid zero. The token cannot see claim balances, so the fix is a scope decision for the requester: accept and document, or move the tax into a pool hook.\n\n2. **Medium: sells are untaxed, so the brief's 3% buy/sell tax is delivered as buy-only.** A 1000 MAD transfer into the PoolManager pays 0 instead of 30 MAD, directly and via an approved router. The README discloses this and its reasoning is correct: a deducted sell tax breaks v4 settlement and fails the launch floor's sell-everything check. I reported it so the requester decides explicitly, for example by raising the buy-side rate to recover the round-trip take, instead of inheriting the deviation from a README paragraph.\n\n3. **Low: outside liquidity providers pay 3% on withdrawing their own MAD principal and LP fees.** Any take of the MAD leg from the PoolManager is treated as a buy. The project's own v4 test asserts this behaviour. It is documented, but it is money taken from a party the economics brief does not name, and it discourages third-party liquidity.\n\n**What held**\n\n- Supply conservation across taxed buys, atomic revert when the pool lacks the gross, and the pool being debited exactly the gross it accounts for.\n- All launch flows arrive whole: factory to distributor, distributor to claimant, factory to remainder address, factory seed into the PoolManager, and a trader can sell its entire bought balance.\n- `previewBuy` matches the real split, rounding is buyer-favouring dust only, constructor mints exactly once to the deployer, tax wallet constant matches the brief, no admin surface, no delegatecall or selfdestruct.\n- `approve` has no tax interaction. Both static-analysis leads were style only.\n\n**Coverage**: all three listed entry points have rows, plus six invariant rows. The only unreached row is the launch manifest, since no `launch.json` exists yet; the README's constructor order and supply match the code.","treeHash":null,"usage":{"cachedInputTokens":889861,"inputTokens":322,"model":"claude-fable-5-1","outputTokens":23619,"runtime":"claude","turns":25,"wallClockMs":400203}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"5835e48821d8827d","findings":[{"citation":"resolved","description":"The 3% buy tax is charged only when an ERC-20 transfer leaves the PoolManager (`from == POOL_MANAGER` in `_update`). Uniswap v4 lets any caller settle a positive swap delta without an ERC-20 transfer: `PoolManager.mint(to, currency.toId(), amount)` credits the trader with ERC-6909 claim tokens and the MAD never leaves the PoolManager's balance, so `_update` is never called and no tax is paid. The same trader later pays a sell with `PoolManager.burn(from, id, amount)`, again with no ERC-20 transfer, and claims are freely transferable between wallets with the PoolManager's ERC-6909 `transfer`/`transferFrom`. A trader (in practice every arbitrage/MEV bot, which generates most of a taxed token's volume) can therefore buy, hold, trade and sell MAD through the pool indefinitely and pay 0% instead of 3%, while an ordinary wallet using `take` pays 3%. This is an economics x asymmetry gap (Pashov Trust Gap seam 2, Asymmetry branch diff: `take` branch taxed, `mint` branch untaxed): the tax formula's result depends on which settlement primitive the caller picks, and the caller always picks the free one. The same path lets a liquidity provider remove the MAD leg of a position untaxed by minting claims, while a naive LP using `take` is taxed 3%. The token cannot observe ERC-6909 accounting, so there is no fix inside `_update`; the requester needs to decide whether an avoidable tax is acceptable (and document that the effective buy tax applies only to ERC-20 withdrawals from the PoolManager), or whether the fee design must move to a venue the token can observe (for example wallet-to-wallet transfers, which it does see), which is a scope decision rather than a code patch.","line":83,"path":"src/MADToken.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {ERC20} from \"@openzeppelin/contracts/token/ERC20/ERC20.sol\";\nimport {IERC20} from \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\nimport {PoolManager} from \"v4-core/src/PoolManager.sol\";\nimport {IPoolManager} from \"v4-core/src/interfaces/IPoolManager.sol\";\nimport {IUnlockCallback} from \"v4-core/src/interfaces/callback/IUnlockCallback.sol\";\nimport {IHooks} from \"v4-core/src/interfaces/IHooks.sol\";\nimport {PoolKey} from \"v4-core/src/types/PoolKey.sol\";\nimport {Currency, CurrencyLibrary} from \"v4-core/src/types/Currency.sol\";\nimport {BalanceDelta} from \"v4-core/src/types/BalanceDelta.sol\";\nimport {ModifyLiquidityParams, SwapParams} from \"v4-core/src/types/PoolOperation.sol\";\nimport {TickMath} from \"v4-core/src/libraries/TickMath.sol\";\nimport {MADToken} from \"src/MADToken.sol\";\n\n/// @notice The paired currency: a plain 18-decimal ERC-20 standing in for FWA.\ncontract PairToken is ERC20 {\n    constructor() ERC20(\"Pair\", \"PAIR\") {}\n\n    function mint(address to, uint256 amount) external {\n        _mint(to, amount);\n    }\n}\n\n/// @notice Seeds a two-sided position the ordinary way (sync / transfer / settle for both legs).\ncontract LiquidityRouter is IUnlockCallback {\n    IPoolManager immutable manager;\n\n    constructor(IPoolManager manager_) {\n        manager = manager_;\n    }\n\n    function modify(PoolKey calldata key, ModifyLiquidityParams calldata params) external returns (BalanceDelta) {\n        return abi.decode(manager.unlock(abi.encode(key, params)), (BalanceDelta));\n    }\n\n    function unlockCallback(bytes calldata data) external returns (bytes memory) {\n        require(msg.sender == address(manager), \"not the pool manager\");\n        (PoolKey memory key, ModifyLiquidityParams memory params) = abi.decode(data, (PoolKey, ModifyLiquidityParams));\n        (BalanceDelta delta,) = manager.modifyLiquidity(key, params, \"\");\n        _pay(key.currency0, delta.amount0());\n        _pay(key.currency1, delta.amount1());\n        return abi.encode(delta);\n    }\n\n    function _pay(Currency currency, int128 delta) internal {\n        require(delta <= 0, \"seed only pays\");\n        if (delta < 0) {\n            manager.sync(currency);\n            IERC20(Currency.unwrap(currency)).transfer(address(manager), uint256(uint128(-delta)));\n            manager.settle();\n        }\n    }\n}\n\n/// @notice A trader that keeps its MAD inside the PoolManager as ERC-6909 claims. Buying mints\n/// claims instead of taking the ERC-20 out; selling burns claims instead of transferring in.\n/// Neither step moves MAD through `MADToken._update`, so the 3% buy tax is never charged.\ncontract ClaimsTrader is IUnlockCallback {\n    using CurrencyLibrary for Currency;\n\n    IPoolManager immutable manager;\n    PoolKey key;\n    Currency mad;\n\n    constructor(IPoolManager manager_, PoolKey memory key_, Currency mad_) {\n        manager = manager_;\n        key = key_;\n        mad = mad_;\n    }\n\n    function swap(bool zeroForOne, int256 amountSpecified) external returns (BalanceDelta) {\n        return abi.decode(manager.unlock(abi.encode(zeroForOne, amountSpecified)), (BalanceDelta));\n    }\n\n    function unlockCallback(bytes calldata data) external returns (bytes memory) {\n        require(msg.sender == address(manager), \"not the pool manager\");\n        (bool zeroForOne, int256 amountSpecified) = abi.decode(data, (bool, int256));\n        uint160 limit = zeroForOne ? TickMath.MIN_SQRT_PRICE + 1 : TickMath.MAX_SQRT_PRICE - 1;\n        BalanceDelta delta = manager.swap(key, SwapParams(zeroForOne, amountSpecified, limit), \"\");\n        _settle(key.currency0, delta.amount0());\n        _settle(key.currency1, delta.amount1());\n        return abi.encode(delta);\n    }\n\n    function _settle(Currency currency, int128 delta) internal {\n        if (delta == 0) return;\n        bool isMad = Currency.unwrap(currency) == Currency.unwrap(mad);\n        if (delta > 0) {\n            uint256 amount = uint256(uint128(delta));\n            if (isMad) {\n                // Buy: keep the MAD inside the PoolManager as a claim. No ERC-20 transfer happens.\n                manager.mint(address(this), currency.toId(), amount);\n            } else {\n                manager.take(currency, address(this), amount);\n            }\n        } else {\n            uint256 amount = uint256(uint128(-delta));\n            if (isMad) {\n                // Sell: pay with claims. No ERC-20 transfer happens.\n                manager.burn(address(this), currency.toId(), amount);\n            } else {\n                manager.sync(currency);\n                IERC20(Currency.unwrap(currency)).transfer(address(manager), amount);\n                manager.settle();\n            }\n        }\n    }\n}\n\n/// @notice Fails on the current MADToken: a buy settled as ERC-6909 claims pays no tax, and the\n/// claims can be sold back (burned) or transferred without ever paying it either.\ncontract MADClaimsBypassTest is Test {\n    using CurrencyLibrary for Currency;\n\n    address constant TAX_WALLET = 0x1EE3ffadfE138415A2ddaedeEBe5F46616fA413d;\n    uint24 constant POOL_FEE = 12_500;\n    int24 constant TICK_SPACING = 60;\n    uint160 constant ONE_TO_ONE = 79_228_162_514_264_337_593_543_950_336;\n\n    PoolManager manager;\n    MADToken token;\n    PairToken pair;\n    LiquidityRouter lp;\n    ClaimsTrader trader;\n    PoolKey key;\n    bool tokenIsZero;\n\n    function setUp() public {\n        manager = new PoolManager(address(this));\n        pair = new PairToken();\n        // This test contract plays the factory: it deploys the token and holds the supply.\n        token = new MADToken(address(this), address(manager));\n        lp = new LiquidityRouter(manager);\n\n        tokenIsZero = address(token) < address(pair);\n        (Currency c0, Currency c1) = tokenIsZero\n            ? (Currency.wrap(address(token)), Currency.wrap(address(pair)))\n            : (Currency.wrap(address(pair)), Currency.wrap(address(token)));\n        key = PoolKey(c0, c1, POOL_FEE, TICK_SPACING, IHooks(address(0)));\n        manager.initialize(key, ONE_TO_ONE);\n\n        token.transfer(address(lp), 10_000_000e18);\n        pair.mint(address(lp), 10_000_000e18);\n        lp.modify(key, ModifyLiquidityParams(-600, 600, 1_000_000e18, bytes32(0)));\n\n        trader = new ClaimsTrader(manager, key, Currency.wrap(address(token)));\n    }\n\n    function _tokenDelta(BalanceDelta delta) internal view returns (int128) {\n        return tokenIsZero ? delta.amount0() : delta.amount1();\n    }\n\n    function test_buyAsClaimsPaysNoTax() public {\n        pair.mint(address(trader), 1_000e18);\n        uint256 poolBefore = token.balanceOf(address(manager));\n        uint256 madId = Currency.wrap(address(token)).toId();\n\n        // Buy: 100 PAIR in, MAD out, kept as ERC-6909 claims.\n        BalanceDelta delta = trader.swap(!tokenIsZero, -100e18);\n        uint256 gross = uint256(uint128(_tokenDelta(delta)));\n        assertGt(gross, 0, \"no MAD came out of the swap\");\n        assertEq(manager.balanceOf(address(trader), madId), gross, \"trader did not get full gross as claims\");\n        assertEq(token.balanceOf(address(manager)), poolBefore, \"no ERC-20 left the PoolManager\");\n\n        // Sell everything back by burning the claims: still no ERC-20 transfer.\n        uint256 pairBefore = pair.balanceOf(address(trader));\n        trader.swap(tokenIsZero, -int256(gross));\n        assertEq(manager.balanceOf(address(trader), madId), 0, \"claims not fully burned\");\n        assertGt(pair.balanceOf(address(trader)), pairBefore, \"no PAIR came back from the sell\");\n\n        // The contract's stated guarantee: every buy from the pool pays 3% of the gross to the tax wallet.\n        // Here a full buy-then-sell round trip through the pool paid nothing.\n        uint256 expectedTax = (gross * token.TAX_BPS()) / token.BPS_DENOMINATOR();\n        assertEq(token.balanceOf(TAX_WALLET), expectedTax, \"buy settled as ERC-6909 claims paid no buy tax\");\n    }\n}","reproduction":"State: MAD/PAIR v4 pool seeded two-sided at 1:1 (as in test/MADTokenV4.t.sol). Trader T holds 100e18 PAIR. 1) T calls manager.unlock; in the callback: manager.swap(key, PAIR->MAD, exactInput -100e18) returns a MAD delta of ~98.7e18; T settles PAIR with sync/transfer/settle and settles MAD by manager.mint(T, Currency.wrap(MAD).toId(), 98.7e18) instead of take. Expected (stated guarantee, README 'Buy: 97% to trader, 3% to tax wallet'): TAX_WALLET balance increases by 2.96e18. Actual: TAX_WALLET balance stays 0, manager.balanceOf(T, madId) == 98.7e18, token.balanceOf(manager) unchanged. 2) T swaps MAD->PAIR exactInput 98.7e18 and settles MAD with manager.burn(T, madId, 98.7e18): the sell settles, T receives PAIR, TAX_WALLET still 0. Compare with the ERC-20 path in test/MADTokenV4.t.sol:test_buyPaysThreePercentOfTheGrossOutput, where the identical swap pays 2.96e18 to TAX_WALLET. Proof test: test/scratch/MADClaimsBypass.t.sol fails with 'buy settled as ERC-6909 claims paid no buy tax: 0 != 2962207482011151398'.","severity":"high","snippet":"        if (from == POOL_MANAGER && !isTaxExempt(to)) {","title":"Buy tax is bypassed entirely by settling swaps as ERC-6909 claims inside the PoolManager"},{"citation":"resolved","description":"The objective asks for a 3% buy/sell tax to 0x1EE3ffadfE138415A2ddaedeEBe5F46616fA413d. In `_update` only the `from == POOL_MANAGER` branch takes a tax; a transfer into the PoolManager (a sell) falls through to the plain `super._update` and arrives whole, so the sell leg pays 0% and the round trip carries 3% instead of the specified 6%. The README documents this and its reasoning is correct: on Uniswap v4 an input is credited by the PoolManager's balance change between sync and settle, so shaving a sell leaves the swap unsettled and reverts (which the launch floor's test_theSeedAndASwapEachWaySucceed would catch), and charging the tax on top of the amount makes a seller who holds exactly x unable to sell x (the floor sells the trader's whole balance). This is therefore a limitation of the requested design on v4, not a coding slip, but it is a deviation from the requester's stated economics and halves the fee stream they asked for, so it must be an explicit requester decision rather than a note in the README. Options that preserve the floor: accept a buy-only tax; raise TAX_BPS to 600 so the round trip carries the intended 6% (changes the per-leg rate the requester specified); or redesign the fee so the sell-side portion is collected somewhere the token can observe. Note that whichever buy-side rate is chosen, finding 1 makes it avoidable through ERC-6909 claims.","line":92,"path":"src/MADToken.sol","reproduction":"State: FACTORY has transferred 1000e18 MAD to ALICE (plain transfer, no tax). Call: ALICE calls token.transfer(POOL_MANAGER, 1000e18). Expected per the brief ('3% buy/sell tax'): TAX_WALLET receives 30e18 and the sale is net 970e18 (or the seller pays 30e18 on top). Actual: balanceOf(POOL_MANAGER) == 1000e18, balanceOf(TAX_WALLET) == 0, no BuyTaxed event. The project's own test test/MADToken.t.sol:test_sellToPoolArrivesWhole asserts this 0 as intended behaviour. The same holds via transferFrom (test_sellViaTransferFromArrivesWhole) and through the real PoolManager (test/MADTokenV4.t.sol:test_sellOfWholeBoughtBalanceSettlesAndIsUntaxed).","severity":"medium","snippet":"        super._update(from, to, value);","title":"Sells are untaxed: the delivered token charges 3% on buys only, not the 3% buy/sell tax the brief specifies"},{"citation":"resolved","description":"The tax trigger is 'ERC-20 leaves the PoolManager to a non-exempt address'. That is true of every `take`, not only swap outputs: a liquidity provider removing a position receives the MAD leg minus 3%, and the pool's accrued MAD swap fees collected by an LP are also taxed. Adding liquidity is untaxed, so the add/remove pair is asymmetric (Asymmetry guide, deposit/withdraw pair) and an LP who adds and immediately removes loses 3% of the MAD leg for nothing. The exemption list in `isTaxExempt` has no way to distinguish an LP withdrawal from a buy because `_update` cannot see the PoolManager's reason for the transfer. Combined with finding 1, a sophisticated LP removes with `mint` (claims) and pays nothing, so the cost falls only on LPs using standard tooling (PositionManager `TAKE_PAIR` to the owner). The launch's own seeded position is unaffected while it is owned by the factory (exempt recipient), and the author documents the behaviour in README 'Side effects'. Reported so the requester decides whether deterring third-party liquidity is acceptable; no token-level fix exists without an LP registry (a setter), which the brief does not provide.","line":68,"path":"src/MADToken.sol","reproduction":"State: pool seeded by LiquidityRouter lp with 1_000_000e18 liquidity around 1:1 (test/MADTokenV4.t.sol setUp). Call: lp.modify(key, ModifyLiquidityParams(-600, 600, -1_000_000e18, bytes32(0))) which takes the MAD leg to lp. Expected for a liquidity withdrawal: lp receives the full MAD leg (gross). Actual: lp receives gross * 0.97 and TAX_WALLET receives gross * 0.03, as asserted in test_removingLiquidityIsTreatedAsABuyOfTheTokenLeg. The same LP settling the MAD leg with manager.mint(lp, madId, gross) receives gross as claims and pays 0 (finding 1).","severity":"low","snippet":"        return account == FACTORY || account == POOL_MANAGER || account == TAX_WALLET;","title":"Liquidity withdrawal is taxed as a buy while liquidity deposit is not, and only naive LPs pay it"}],"hash":"46fa8ec563209a195a8ee5aa98bb3a53cff77bca07f8ff54bbe4a16377c026a4","nodeId":"a2751531-d591-46c1-a23b-28a4d347ef71","outcome":"completed","summary":"The review is complete and `.imd-findings.json` holds three findings plus a nine-row coverage record. Git status is clean because the findings file and `test/scratch/` are ignored, which is the expected state for a review that changes no project files.\n\n**What I found in the Access Control, Trust Gap and Asymmetry areas**\n\n- **High: buy tax bypassed through ERC-6909 claims.** The tax fires only when an ERC-20 transfer leaves the PoolManager. A trader who settles a swap with `PoolManager.mint` keeps the MAD as claim tokens, pays no tax, and can later sell by burning claims, again untaxed. This is the Trust Gap seam between economics and asymmetry: the fee depends on which settlement primitive the caller picks. The scratch proof test in `test/scratch/MADClaimsBypass.t.sol` runs a full buy-then-sell round trip through the real PoolManager and fails with the tax wallet at zero instead of about 2.96 MAD. There is no fix inside the token, so this needs a requester decision on the fee design.\n- **Medium: sells are untaxed.** The brief asks for a 3% buy/sell tax. The sell leg takes the plain branch and arrives whole, so the round trip carries 3% rather than 6%. The README's reasoning that a deducted sell tax breaks v4 settlement and the launch floor is correct, but the deviation from the requester's stated economics should be their explicit choice.\n- **Low: liquidity withdrawal taxed, deposit not.** Removing a position's MAD leg via `take` pays 3% while adding pays nothing, and an LP using claims avoids it entirely. Documented by the author, reported as a deterrent to third-party liquidity.\n\n**What holds.** All three listed entry points are standard OpenZeppelin v5.1 code. The constructor mints exactly the manifest supply once to the factory with no roles handed out. There is no owner, setter, pause, blacklist, mint or burn, and the launch flows and PoolManager accounting are exact in both directions. The existing 36 project tests pass. Static-analysis leads were both cosmetic and not reported.\n\n**Not reached.** The ProjectFactory source is not in the tree, so I could not confirm whether tokens a trader `take`s to the exempt factory address are ever recoverable. I noted it as unverifiable rather than as a finding.","treeHash":null,"usage":{"cachedInputTokens":884225,"inputTokens":354,"model":"claude-fable-5-1","outputTokens":26723,"runtime":"claude","turns":28,"wallClockMs":381673}},{"artifacts":[],"attempt":1,"bundleHash":"f187c0490dd4a2c9a986b91497765b8a2c4cb367b9dd78600402d9c37c8e92fb","device":"d0653dc91b6e2259","findings":[{"description":"The brief specifies a '3% buy/sell tax that goes to 0x1EE3ffadfE138415A2ddaedeEBe5F46616fA413d'. _update only taxes transfers whose sender is the PoolManager (buys); a transfer into the PoolManager (a sell) moves whole and pays nothing. The README states this deliberately (a deducted tax on a transfer into the v4 PoolManager would leave the swap unsettled), but the requester receives half of the tax they specified: a buy+sell round trip pays 3% instead of ~6%. This is a requirement/implementation mismatch the requester must decide on (accept buy-only, raise the buy rate, or collect the sell side some other way, e.g. a fee-taking pool hook), not something the tests can assert as correct. The suites delivered here assert that every buy pays at least its 3% and that every sell settles (the launch floor), and deliberately do not assert that sells are untaxed.","line":83,"path":"src/MADToken.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {ERC20} from \"@openzeppelin/contracts/token/ERC20/ERC20.sol\";\nimport {IERC20} from \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\nimport {PoolManager} from \"v4-core/src/PoolManager.sol\";\nimport {IPoolManager} from \"v4-core/src/interfaces/IPoolManager.sol\";\nimport {IUnlockCallback} from \"v4-core/src/interfaces/callback/IUnlockCallback.sol\";\nimport {IHooks} from \"v4-core/src/interfaces/IHooks.sol\";\nimport {PoolKey} from \"v4-core/src/types/PoolKey.sol\";\nimport {Currency} from \"v4-core/src/types/Currency.sol\";\nimport {BalanceDelta} from \"v4-core/src/types/BalanceDelta.sol\";\nimport {ModifyLiquidityParams, SwapParams} from \"v4-core/src/types/PoolOperation.sol\";\nimport {TickMath} from \"v4-core/src/libraries/TickMath.sol\";\nimport {MADToken} from \"src/MADToken.sol\";\n\ncontract ProofPair is ERC20 {\n    constructor() ERC20(\"Pair\", \"PAIR\") {}\n\n    function mint(address to, uint256 amount) external {\n        _mint(to, amount);\n    }\n}\n\ncontract ProofActor is IUnlockCallback {\n    IPoolManager immutable manager;\n\n    constructor(IPoolManager manager_) {\n        manager = manager_;\n    }\n\n    function _settle(Currency currency, int128 delta) internal {\n        if (delta < 0) {\n            manager.sync(currency);\n            IERC20(Currency.unwrap(currency)).transfer(address(manager), uint256(uint128(-delta)));\n            manager.settle();\n        } else if (delta > 0) {\n            manager.take(currency, address(this), uint256(uint128(delta)));\n        }\n    }\n\n    function addLiquidity(PoolKey calldata key, int256 liquidity) external {\n        manager.unlock(abi.encode(uint8(0), key, liquidity, false));\n    }\n\n    function swap(PoolKey calldata key, bool zeroForOne, int256 amount) external {\n        manager.unlock(abi.encode(uint8(1), key, amount, zeroForOne));\n    }\n\n    function unlockCallback(bytes calldata data) external returns (bytes memory) {\n        (uint8 kind, PoolKey memory key, int256 amount, bool zeroForOne) = abi.decode(data, (uint8, PoolKey, int256, bool));\n        BalanceDelta delta;\n        if (kind == 0) {\n            (delta,) = manager.modifyLiquidity(key, ModifyLiquidityParams(-60_000, 60_000, amount, bytes32(0)), \"\");\n        } else {\n            uint160 limit = zeroForOne ? TickMath.MIN_SQRT_PRICE + 1 : TickMath.MAX_SQRT_PRICE - 1;\n            delta = manager.swap(key, SwapParams(zeroForOne, amount, limit), \"\");\n        }\n        _settle(key.currency0, delta.amount0());\n        _settle(key.currency1, delta.amount1());\n        return \"\";\n    }\n}\n\n/// The brief: \"3% buy/sell tax that goes to 0x1EE3...413d\". A sell of MAD into the v4 pool pays nothing.\ncontract SellTaxProof is Test {\n    address constant TAX_WALLET = 0x1EE3ffadfE138415A2ddaedeEBe5F46616fA413d;\n\n    function test_aSellIntoThePoolPaysTheThreePercentTax() public {\n        PoolManager manager = new PoolManager(address(this));\n        ProofPair pair = new ProofPair();\n        MADToken token = new MADToken(address(this), address(manager));\n        bool tokenIsZero = address(token) < address(pair);\n        PoolKey memory key = tokenIsZero\n            ? PoolKey(Currency.wrap(address(token)), Currency.wrap(address(pair)), 12_500, 60, IHooks(address(0)))\n            : PoolKey(Currency.wrap(address(pair)), Currency.wrap(address(token)), 12_500, 60, IHooks(address(0)));\n        manager.initialize(key, 79_228_162_514_264_337_593_543_950_336);\n\n        ProofActor lp = new ProofActor(manager);\n        token.transfer(address(lp), 10_000_000e18);\n        pair.mint(address(lp), 10_000_000e18);\n        lp.addLiquidity(key, 1_000_000e18);\n\n        // The seller got its MAD off-pool (e.g. a distributor claim), so no buy tax is involved.\n        ProofActor seller = new ProofActor(manager);\n        token.transfer(address(seller), 1_000e18);\n        uint256 taxBefore = token.balanceOf(TAX_WALLET);\n\n        seller.swap(key, tokenIsZero, -1_000e18); // sell 1,000 MAD, exact input\n\n        // Expected: the tax wallet receives 3% of the 1,000 MAD sold (30 MAD). Actual: 0.\n        assertGe(token.balanceOf(TAX_WALLET) - taxBefore, 30e18, \"a 1,000 MAD sell paid no sell tax\");\n    }\n}","reproduction":"Real v4 PoolManager, MAD/pair pool at 1:1, fee 12500, tick spacing 60, liquidity 1e24 over ticks [-60000, 60000]. A trader holding 1,000 MAD (received off-pool) swaps exact-input 1,000 MAD for the pair token. Expected per brief: tax wallet +30 MAD. Actual: tax wallet +0 (proof fails with 'a 1,000 MAD sell paid no sell tax: 0 < 30000000000000000000').","severity":"medium","title":"Sell tax required by the brief is not implemented: sells into the pool pay 0% instead of 3%"},{"description":"The buy tax is charged only when MAD physically leaves the PoolManager (from == POOL_MANAGER). A router that settles a buy with manager.mint (ERC-6909 claim) instead of manager.take never moves MAD, so no tax is charged. Claims can be transferred between accounts and spent on a later sell (manager.burn) without any MAD transfer either, so a trader who stays in claims trades MAD with zero tax in both directions. Only converting a claim back to ERC-20 MAD (take) is taxed. Combined with the untaxed sell this lets sophisticated traders avoid the tax entirely.","line":83,"path":"src/MADToken.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {ERC20} from \"@openzeppelin/contracts/token/ERC20/ERC20.sol\";\nimport {IERC20} from \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\nimport {PoolManager} from \"v4-core/src/PoolManager.sol\";\nimport {IPoolManager} from \"v4-core/src/interfaces/IPoolManager.sol\";\nimport {IUnlockCallback} from \"v4-core/src/interfaces/callback/IUnlockCallback.sol\";\nimport {IHooks} from \"v4-core/src/interfaces/IHooks.sol\";\nimport {PoolKey} from \"v4-core/src/types/PoolKey.sol\";\nimport {Currency, CurrencyLibrary} from \"v4-core/src/types/Currency.sol\";\nimport {BalanceDelta} from \"v4-core/src/types/BalanceDelta.sol\";\nimport {ModifyLiquidityParams, SwapParams} from \"v4-core/src/types/PoolOperation.sol\";\nimport {TickMath} from \"v4-core/src/libraries/TickMath.sol\";\nimport {MADToken} from \"src/MADToken.sol\";\n\ncontract ClaimsPair is ERC20 {\n    constructor() ERC20(\"Pair\", \"PAIR\") {}\n\n    function mint(address to, uint256 amount) external {\n        _mint(to, amount);\n    }\n}\n\n/// Settles inputs normally, but keeps a MAD output as an ERC-6909 claim (manager.mint) instead of take.\ncontract ClaimsActor is IUnlockCallback {\n    using CurrencyLibrary for Currency;\n\n    IPoolManager immutable manager;\n    address immutable mad;\n\n    constructor(IPoolManager manager_, address mad_) {\n        manager = manager_;\n        mad = mad_;\n    }\n\n    function _settle(Currency currency, int128 delta) internal {\n        if (delta < 0) {\n            manager.sync(currency);\n            IERC20(Currency.unwrap(currency)).transfer(address(manager), uint256(uint128(-delta)));\n            manager.settle();\n        } else if (delta > 0) {\n            if (Currency.unwrap(currency) == mad) manager.mint(address(this), currency.toId(), uint256(uint128(delta)));\n            else manager.take(currency, address(this), uint256(uint128(delta)));\n        }\n    }\n\n    function addLiquidity(PoolKey calldata key, int256 liquidity) external {\n        manager.unlock(abi.encode(uint8(0), key, liquidity, false));\n    }\n\n    function swap(PoolKey calldata key, bool zeroForOne, int256 amount) external {\n        manager.unlock(abi.encode(uint8(1), key, amount, zeroForOne));\n    }\n\n    function unlockCallback(bytes calldata data) external returns (bytes memory) {\n        (uint8 kind, PoolKey memory key, int256 amount, bool zeroForOne) = abi.decode(data, (uint8, PoolKey, int256, bool));\n        BalanceDelta delta;\n        if (kind == 0) {\n            (delta,) = manager.modifyLiquidity(key, ModifyLiquidityParams(-60_000, 60_000, amount, bytes32(0)), \"\");\n        } else {\n            uint160 limit = zeroForOne ? TickMath.MIN_SQRT_PRICE + 1 : TickMath.MAX_SQRT_PRICE - 1;\n            delta = manager.swap(key, SwapParams(zeroForOne, amount, limit), \"\");\n        }\n        _settle(key.currency0, delta.amount0());\n        _settle(key.currency1, delta.amount1());\n        return \"\";\n    }\n}\n\n/// A buy whose MAD output is kept as a v4 ERC-6909 claim never moves MAD out of the PoolManager,\n/// so the 3% buy tax is never charged on it.\ncontract ClaimsBypassProof is Test {\n    using CurrencyLibrary for Currency;\n\n    address constant TAX_WALLET = 0x1EE3ffadfE138415A2ddaedeEBe5F46616fA413d;\n\n    function test_aBuyHeldAsAClaimPaysTheBuyTax() public {\n        PoolManager manager = new PoolManager(address(this));\n        ClaimsPair pair = new ClaimsPair();\n        MADToken token = new MADToken(address(this), address(manager));\n        bool tokenIsZero = address(token) < address(pair);\n        PoolKey memory key = tokenIsZero\n            ? PoolKey(Currency.wrap(address(token)), Currency.wrap(address(pair)), 12_500, 60, IHooks(address(0)))\n            : PoolKey(Currency.wrap(address(pair)), Currency.wrap(address(token)), 12_500, 60, IHooks(address(0)));\n        manager.initialize(key, 79_228_162_514_264_337_593_543_950_336);\n\n        ClaimsActor lp = new ClaimsActor(manager, address(token));\n        token.transfer(address(lp), 10_000_000e18);\n        pair.mint(address(lp), 10_000_000e18);\n        lp.addLiquidity(key, 1_000_000e18);\n\n        ClaimsActor buyer = new ClaimsActor(manager, address(token));\n        pair.mint(address(buyer), 1_000e18);\n        buyer.swap(key, !tokenIsZero, -1_000e18); // buy MAD with 1,000 pair, exact input\n\n        uint256 bought = manager.balanceOf(address(buyer), Currency.wrap(address(token)).toId());\n        assertGt(bought, 900e18, \"the buyer received its MAD as a claim\");\n        // Expected: 3% of the MAD bought goes to the tax wallet. Actual: 0.\n        assertGe(token.balanceOf(TAX_WALLET), (bought * 300) / 9_700, \"a buy settled as a claim paid no buy tax\");\n    }\n}","reproduction":"Same pool as above. A buyer swaps exact-input 1,000 pair for MAD and settles the MAD output with manager.mint(buyer, id(MAD), delta). Buyer receives ~1,017 MAD of claims. Expected: tax wallet receives 3% of the gross output (~30.5 MAD). Actual: 0 (proof fails with 'a buy settled as a claim paid no buy tax: 0 < 30511107394849647828').","severity":"low","title":"Buy tax is bypassed when the MAD output is kept as a Uniswap v4 ERC-6909 claim"},{"description":"Any MAD leaving the PoolManager to a non-exempt address is treated as a buy. Removing liquidity, collecting MAD fees, and even adding to an existing position whose accrued MAD fees exceed the deposit (the net delta pays out) all deliver 97% to the LP and 3% to the tax wallet. Only the factory is exempt. Third-party LPs therefore lose 3% of their MAD principal on exit; worth stating to the requester and to front-ends. The invariant suite test/MADTokenV4Invariant.t.sol models this explicitly (it surfaced the fee-payout-on-add case).","line":83,"path":"src/MADToken.sol","reproduction":"Add liquidity over [-60000,60000], let trades accrue MAD fees, then call modifyLiquidity with a small positive liquidityDelta: the MAD delta is positive and the take of X MAD delivers 0.97X to the LP and 0.03X to 0x1EE3...413d (observed: take of 43280037848608777 wei delivered 41981636713150514, tax 1298401135458263).","severity":"info","title":"Liquidity providers pay the 3% buy tax on every MAD they withdraw, including earned fees"}],"hash":"47040b3a50a594abbd97356664e4edb4798507246d4cd033698d08b266e86a21","nodeId":"1a9f81e8-15f4-4bd5-9a4f-8a731d9d9bf4","outcome":"completed","summary":"I added three test files. `forge build` and `forge test` pass with the verifier's fuzz seed: 60 tests in 5 suites, 0 failures, in about 10 seconds. I also reported three findings in `.imd-findings.json`. The biggest is that **sells are not taxed, although the brief asks for a 3% buy *and* sell tax**.\n\n**New tests (all in `test/`):**\n- **`MADTokenInvariant.t.sol`**: random sequences of transfers, approvals and `transferFrom` calls (256 runs × depth 60), involving the factory, the pool manager, the tax wallet and ordinary holders. They check that:\n  - supply never changes, and balances always add up to it;\n  - the pool manager is charged the full amount of every buy and receives the full amount of every sell;\n  - every buy pays its 3% to the tax wallet;\n  - a failed transfer leaves no partial tax behind.\n- **`MADTokenV4Invariant.t.sol`**: the same idea against the real Uniswap v4 PoolManager (128 runs × depth 50). It covers buys by amount spent and by amount wanted, partial and full sells, wallet transfers, and adding and removing liquidity. A running record of what each holder and the pool manager should hold is compared after every step, and every sell must settle. At the end of each run, every trader sells their whole balance and all liquidity is withdrawn, which shows the pool is never left short.\n- **`MADTokenEdges.t.sol`**: 22 edge-case and failure-path tests. These include a zero buy, 1 wei, the 33/34-wei rounding cut-off, buying the whole supply, sending to the zero address, buying from an empty pool, allowance checks, unlimited allowances, and the size limits of `previewBuy`. Three of them are fuzz tests over the full supply range at 1,000 runs each.\n\nI left the earlier contributors' two test files unchanged. Two of their tests, `test_sellOfWholeBoughtBalanceSettlesAndIsUntaxed` and `test_sellToPoolArrivesWhole`, assert that sells are untaxed. My tests check only that every buy pays *at least* its 3% and that sells go through, so they don't treat the missing sell tax as correct.\n\n**Findings:**\n1. **Medium – no sell tax.** `_update` (`src/MADToken.sol:83`) only taxes tokens leaving the pool. A trader selling 1,000 MAD pays 0 instead of 30 MAD, so the requester gets half the tax they asked for. The README says this is deliberate: on Uniswap v4, taking a cut of tokens sent into the pool would make the sale fail. The requester still needs to decide what to do: accept buy-only, raise the buy rate, or collect the sell side another way. A proof test is included that fails on the current code.\n2. **Low – the buy tax can be avoided.** A trading contract can keep the bought MAD as a credit inside the v4 pool manager (an ERC-6909 claim) instead of withdrawing it. No MAD moves, so no tax is charged; the proof shows about 30.5 MAD of tax skipped on one buy. Since those credits can also be spent on later sells, someone who never withdraws trades MAD tax-free in both directions. A failing proof test is included.\n3. **Info – liquidity providers pay 3% on every MAD they withdraw.** This includes their principal and earned fees, and even adding to a position when its accrued fees exceed the deposit. The invariant run turned up that last case.","treeHash":"49ef037e4e9c7cccf7c8efab979f9c561c69d49b","usage":{"cachedInputTokens":1273705,"inputTokens":38,"model":"claude-opus-5-5","outputTokens":38091,"runtime":"claude","turns":20,"wallClockMs":571741}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"4be5e039e1c77ce3","findings":[{"citation":"resolved","description":"Boundary: the tax is keyed on an ERC-20 transfer whose sender is the PoolManager (line 83). The calling code assumes every buy leaves the PoolManager through such a transfer. Actual v4 behaviour: a locker may settle a positive MAD delta with PoolManager.mint(to, currency.toId(), amount) (an ERC-6909 claim) instead of take(), and a negative delta with PoolManager.burn(from, id, amount) instead of sync/transfer/settle. Neither path calls MADToken at all, so _update never runs, no BuyTaxed event is emitted and TAX_WALLET receives nothing. The claim is a transferable bearer position inside the PoolManager (ERC-6909 transfer/transferFrom), so a trader can buy, hold, sell, and even pass MAD exposure to other addresses without ever paying the 3%. Only a final take() to an ERC-20 balance is taxed, and a trader who trades within the PoolManager never needs one. Impact: the tax the requester commissioned is avoidable by any unprivileged caller using a standard v4 settlement mode (the Universal Router exposes it as the claims flag on SETTLE/TAKE), so the fee recipient at 0x1EE3ffadfE138415A2ddaedeEBe5F46616fA413d is shorted on exactly the trades most likely to be large and repeated. The token alone cannot observe claim mints, so this cannot be closed inside MADToken.sol; it is a scope decision for the requester: accept and document it in the README (currently silent on claims), or move the tax into a Uniswap v4 hook that charges on swap deltas, which is a different launch kind. Guide: Boundary Agent step 2 (assumption vs actual at the PoolManager boundary); Numerical Gap seam 3 (boundary x invariant: the invariant 'every pool exit of X pays floor(0.03X)' is skipped by a settlement path that never reaches the body).","line":83,"path":"src/MADToken.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {ERC20} from \"@openzeppelin/contracts/token/ERC20/ERC20.sol\";\nimport {IERC20} from \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\nimport {PoolManager} from \"v4-core/src/PoolManager.sol\";\nimport {IPoolManager} from \"v4-core/src/interfaces/IPoolManager.sol\";\nimport {IUnlockCallback} from \"v4-core/src/interfaces/callback/IUnlockCallback.sol\";\nimport {IHooks} from \"v4-core/src/interfaces/IHooks.sol\";\nimport {PoolKey} from \"v4-core/src/types/PoolKey.sol\";\nimport {Currency, CurrencyLibrary} from \"v4-core/src/types/Currency.sol\";\nimport {BalanceDelta} from \"v4-core/src/types/BalanceDelta.sol\";\nimport {ModifyLiquidityParams, SwapParams} from \"v4-core/src/types/PoolOperation.sol\";\nimport {TickMath} from \"v4-core/src/libraries/TickMath.sol\";\nimport {MADToken} from \"src/MADToken.sol\";\n\ncontract PairToken is ERC20 {\n    constructor() ERC20(\"Pair\", \"PAIR\") {}\n\n    function mint(address to, uint256 amount) external {\n        _mint(to, amount);\n    }\n}\n\ncontract LiquidityRouter is IUnlockCallback {\n    IPoolManager immutable manager;\n\n    constructor(IPoolManager manager_) {\n        manager = manager_;\n    }\n\n    function modify(PoolKey calldata key, ModifyLiquidityParams calldata params) external returns (BalanceDelta) {\n        return abi.decode(manager.unlock(abi.encode(key, params)), (BalanceDelta));\n    }\n\n    function unlockCallback(bytes calldata data) external returns (bytes memory) {\n        require(msg.sender == address(manager), \"not the pool manager\");\n        (PoolKey memory key, ModifyLiquidityParams memory params) = abi.decode(data, (PoolKey, ModifyLiquidityParams));\n        (BalanceDelta delta,) = manager.modifyLiquidity(key, params, \"\");\n        _settle(key.currency0, delta.amount0());\n        _settle(key.currency1, delta.amount1());\n        return abi.encode(delta);\n    }\n\n    function _settle(Currency currency, int128 delta) internal {\n        if (delta < 0) {\n            manager.sync(currency);\n            IERC20(Currency.unwrap(currency)).transfer(address(manager), uint256(uint128(-delta)));\n            manager.settle();\n        } else if (delta > 0) {\n            manager.take(currency, address(this), uint256(uint128(delta)));\n        }\n    }\n}\n\n/// @notice A trader that settles the MAD leg with ERC-6909 claims (mint on buy, burn on sell)\n/// instead of ERC-20 take/settle. The pair leg is settled with plain ERC-20 transfers.\ncontract ClaimsTrader is IUnlockCallback {\n    using CurrencyLibrary for Currency;\n\n    IPoolManager immutable manager;\n    Currency immutable mad;\n    PoolKey key;\n\n    constructor(IPoolManager manager_, Currency mad_) {\n        manager = manager_;\n        mad = mad_;\n    }\n\n    function swap(PoolKey calldata key_, bool zeroForOne, int256 amountSpecified) external returns (BalanceDelta) {\n        key = key_;\n        return abi.decode(manager.unlock(abi.encode(zeroForOne, amountSpecified)), (BalanceDelta));\n    }\n\n    function unlockCallback(bytes calldata data) external returns (bytes memory) {\n        require(msg.sender == address(manager), \"not the pool manager\");\n        (bool zeroForOne, int256 amountSpecified) = abi.decode(data, (bool, int256));\n        uint160 limit = zeroForOne ? TickMath.MIN_SQRT_PRICE + 1 : TickMath.MAX_SQRT_PRICE - 1;\n        BalanceDelta delta = manager.swap(key, SwapParams(zeroForOne, amountSpecified, limit), \"\");\n        _settle(key.currency0, delta.amount0());\n        _settle(key.currency1, delta.amount1());\n        return abi.encode(delta);\n    }\n\n    function _settle(Currency currency, int128 delta) internal {\n        if (Currency.unwrap(currency) == Currency.unwrap(mad)) {\n            // MAD leg: claims only. No ERC-20 transfer ever leaves or enters the PoolManager.\n            if (delta < 0) manager.burn(address(this), currency.toId(), uint256(uint128(-delta)));\n            else if (delta > 0) manager.mint(address(this), currency.toId(), uint256(uint128(delta)));\n        } else {\n            if (delta < 0) {\n                manager.sync(currency);\n                IERC20(Currency.unwrap(currency)).transfer(address(manager), uint256(uint128(-delta)));\n                manager.settle();\n            } else if (delta > 0) {\n                manager.take(currency, address(this), uint256(uint128(delta)));\n            }\n        }\n    }\n}\n\n/// @notice Proof: a buy whose MAD output is settled as an ERC-6909 claim pays no tax, and the\n/// claim can be sold back for the pair token, so a full buy/sell round trip pays 0 to TAX_WALLET.\ncontract ClaimsBypassProof is Test {\n    using CurrencyLibrary for Currency;\n\n    address constant TAX_WALLET = 0x1EE3ffadfE138415A2ddaedeEBe5F46616fA413d;\n    uint160 constant ONE_TO_ONE = 79_228_162_514_264_337_593_543_950_336;\n\n    PoolManager manager;\n    MADToken token;\n    PairToken pair;\n    LiquidityRouter lp;\n    ClaimsTrader trader;\n    PoolKey key;\n    bool tokenIsZero;\n\n    function setUp() public {\n        manager = new PoolManager(address(this));\n        pair = new PairToken();\n        token = new MADToken(address(this), address(manager));\n        lp = new LiquidityRouter(manager);\n        trader = new ClaimsTrader(manager, Currency.wrap(address(token)));\n\n        tokenIsZero = address(token) < address(pair);\n        (Currency c0, Currency c1) = tokenIsZero\n            ? (Currency.wrap(address(token)), Currency.wrap(address(pair)))\n            : (Currency.wrap(address(pair)), Currency.wrap(address(token)));\n        key = PoolKey(c0, c1, 12_500, 60, IHooks(address(0)));\n        manager.initialize(key, ONE_TO_ONE);\n\n        token.transfer(address(lp), 10_000_000e18);\n        pair.mint(address(lp), 10_000_000e18);\n        lp.modify(key, ModifyLiquidityParams(-600, 600, 1_000_000e18, bytes32(0)));\n    }\n\n    function test_buySettledAsClaimPaysTheTax() public {\n        pair.mint(address(trader), 1_000e18);\n        uint256 madId = Currency.wrap(address(token)).toId();\n\n        // Buy: exact output of 100 MAD, settled as an ERC-6909 claim.\n        trader.swap(key, !tokenIsZero, 100e18);\n        assertEq(manager.balanceOf(address(trader), madId), 100e18, \"trader holds the whole 100 MAD as a claim\");\n        assertEq(token.balanceOf(address(trader)), 0);\n\n        // Sell the whole claim back for pair tokens.\n        uint256 pairBefore = pair.balanceOf(address(trader));\n        trader.swap(key, tokenIsZero, -100e18);\n        assertEq(manager.balanceOf(address(trader), madId), 0);\n        assertGt(pair.balanceOf(address(trader)), pairBefore, \"the sell paid out\");\n\n        // Expected under a 3% buy tax: 3 MAD to the tax wallet. Actual: 0.\n        assertEq(token.balanceOf(TAX_WALLET), 3e18, \"buy settled as a claim paid no tax\");\n    }\n}","reproduction":"State: MADToken deployed with the real v4 PoolManager, MAD/PAIR pool at 1:1 with liquidity. Trader holds 1000 PAIR. Call 1 (inside unlock): swap exact-output 100e18 MAD, settle PAIR input by sync/transfer/settle, settle the +100e18 MAD delta with manager.mint(trader, MAD.toId(), 100e18). Result: manager.balanceOf(trader, MAD.toId()) == 100e18, token.balanceOf(trader) == 0, token.balanceOf(TAX_WALLET) == 0 (expected 3e18 under a 3% buy tax). Call 2: swap exact-input 100e18 MAD for PAIR, settle the -100e18 MAD delta with manager.burn(trader, MAD.toId(), 100e18), take PAIR. Result: trader's PAIR balance rose, claim is 0, TAX_WALLET still 0. Run: forge test --match-path test/scratch/ClaimsBypassProof.t.sol (fails: 'buy settled as a claim paid no tax: 0 != 3000000000000000000').","severity":"medium","snippet":"        if (from == POOL_MANAGER && !isTaxExempt(to)) {","title":"Buy tax is bypassed when the MAD output is settled as an ERC-6909 claim: a full v4 buy/sell round trip pays 0 to TAX_WALLET"},{"citation":"resolved","description":"The brief commissions a 3% buy/sell tax paid to 0x1EE3ffadfE138415A2ddaedeEBe5F46616fA413d. _update only splits transfers whose sender is the PoolManager (line 83); a transfer whose recipient is the PoolManager falls through to the plain update at line 92, so a sell pays nothing and a buy+sell round trip carries 3% instead of the 6% the requester specified. The README discloses this and gives the reason: v4 credits a sell by the PoolManager's balance change between sync and settle, so a token that shaves the transfer leaves the swap short and the launch floor's sell-everything check (Token.protected.t.sol, test_theSeedAndASwapEachWaySucceed) would fail; charging on top is impossible for a seller holding exactly x. That reasoning is correct for a token-level tax, so this is not a coding error but an unmet requirement that needs the requester's decision: (a) accept buy-only at 3% (current build), (b) raise TAX_BPS to 600 so the round trip carries the intended 6% on the buy leg (one-constant change, still passes the floor), or (c) tax sells in a v4 hook, which is a different launch kind. Reported so the judge records the explicit scope decision rather than treating the README note as acceptance.","line":92,"path":"src/MADToken.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {MADToken} from \"src/MADToken.sol\";\n\n/// @notice Proof: the brief asks for a 3% buy/sell tax; a sell (transfer into the PoolManager)\n/// pays nothing to TAX_WALLET. Also shows the round trip buy+sell carries 3%, not 6%.\ncontract SellTaxProof is Test {\n    address constant TAX_WALLET = 0x1EE3ffadfE138415A2ddaedeEBe5F46616fA413d;\n    address constant FACTORY = address(0xFAC7);\n    address constant POOL_MANAGER = address(0x900C);\n    address constant ALICE = address(0xA11CE);\n\n    MADToken token;\n\n    function setUp() public {\n        vm.prank(FACTORY);\n        token = new MADToken(FACTORY, POOL_MANAGER);\n        vm.prank(FACTORY);\n        token.transfer(ALICE, 1000e18);\n    }\n\n    function test_sellPaysThreePercentToTaxWallet() public {\n        vm.prank(ALICE);\n        token.transfer(POOL_MANAGER, 1000e18);\n        // Expected under the brief's 3% sell tax: 30 MAD to the tax wallet. Actual: 0.\n        assertEq(token.balanceOf(TAX_WALLET), 30e18, \"sell was not taxed\");\n    }\n}","reproduction":"State: token deployed with FACTORY=0xFAC7, POOL_MANAGER=0x900C; FACTORY transfers 1000e18 to ALICE. Call: vm.prank(ALICE); token.transfer(POOL_MANAGER, 1000e18). Expected under the brief: TAX_WALLET balance 30e18, PoolManager 970e18. Actual: TAX_WALLET 0, PoolManager 1000e18. Also reproduced against the real PoolManager by the tree's own test test_sellOfWholeBoughtBalanceSettlesAndIsUntaxed (test/MADTokenV4.t.sol:158), which asserts the tax wallet is unchanged by a sell. Run: forge test --match-path test/scratch/SellTaxProof.t.sol (fails: 'sell was not taxed: 0 != 30000000000000000000').","severity":"medium","snippet":"        super._update(from, to, value);","title":"Sell leg is untaxed: the brief's 3% buy/sell tax collects 0 on every transfer into the PoolManager"},{"citation":"resolved","description":"Boundary: 'sender is the PoolManager' is used as the definition of a buy, but the PoolManager also pays out the MAD leg of modifyLiquidity with negative liquidityDelta, collected swap fees, and donated amounts, all through take(). Each of these is charged 3% to the tax wallet although no purchase occurred. The README lists this as a side effect, but the number matters for the launch itself: the factory seeds economics.poolBps = 8000 of the supply (800,000,000 MAD) single-sided. If that position is ever withdrawn to any address other than FACTORY, POOL_MANAGER or TAX_WALLET (for example the requester's wallet, or a PositionManager that takes to itself), 3% of the remaining MAD leg, up to 24,000,000 MAD, moves to the tax wallet instead of the position owner. Whether the launch position can be withdrawn to a non-exempt address depends on the factory's custody of it, which is outside this tree; the author should state it in the README. Minimal fix inside the token is not possible without a venue signal (the token cannot distinguish take() after swap from take() after modifyLiquidity); the fix is documentation and, if the requester wants LPs untaxed, a hook-based design. Reported low because it is disclosed and the party losing value (the LP) chose to provide liquidity knowing it.","line":83,"path":"src/MADToken.sol","reproduction":"State: real PoolManager, LiquidityRouter holds a position of liquidity 1,000,000e18 in [-600, 600] at 1:1. Call: lp.modify(key, ModifyLiquidityParams(-600, 600, -1_000_000e18, 0)); the router takes both legs to itself. Expected for a withdrawal: LP's MAD balance rises by the full MAD delta (gross). Actual: rises by gross - floor(gross*300/10000) and TAX_WALLET gains floor(gross*300/10000). The tree's own test test_removingLiquidityIsTreatedAsABuyOfTheTokenLeg (test/MADTokenV4.t.sol:183) asserts exactly this split.","severity":"low","snippet":"        if (from == POOL_MANAGER && !isTaxExempt(to)) {","title":"Every ERC-20 exit from the PoolManager is taxed as a buy, including liquidity withdrawal and fee collection: an LP removing 1,000,000 MAD receives 970,000"},{"citation":"resolved","description":"Math Precision: fees should round up (toward the fee recipient); this fee rounds down, so each buy under-collects by up to 0.99 wei and any gross of 1..33 wei pays nothing (33*300/10000 = 0). Numerical Gap seam 1 (precision x invariant): the invariant 'tax collected == 3% of gross bought' holds per buy only up to truncation, and the loss accumulates with the number of buys: 100 buys of 33 wei pay 0 while one buy of 3300 wei pays 99 wei. Overflow is not reachable in _update (gross <= 1e27, 1e27 * 300 << 2^256); previewBuy(buyer, gross) reverts with Panic(0x11) for gross > 2^256/300 instead of returning, which is harmless for a view on an 18-decimal token with 1e27 supply. Impact is dust: at 18 decimals, 33 wei is far below the gas cost of a swap, so nobody can profitably avoid the tax this way, and there is no compounding. Minimal fix if the author wants the fee-favouring direction: tax = (grossAmount * TAX_BPS + BPS_DENOMINATOR - 1) / BPS_DENOMINATOR (ceil), which makes a 1-wei buy deliver 0 net and 1 wei tax; the README's '34 wei' sentence would then change to 'every non-zero buy pays at least 1 wei'.","line":96,"path":"src/MADToken.sol","reproduction":"State: FACTORY holds the supply and sends it all to POOL_MANAGER (0x900C). Calls from POOL_MANAGER: transfer(ALICE, 33) x100 -> ALICE 3300, TAX_WALLET 0; transfer(BOB, 3300) -> TAX_WALLET 99. transfer(ALICE, 66): true tax 1.98 wei, charged 1 (ALICE 65). previewBuy(ALICE, type(uint256).max) reverts (arithmetic overflow) while previewBuy(ALICE, type(uint256).max / 300) returns. Expected under 'fees round up': 33 wei -> 1 wei tax, 66 wei -> 2 wei. Reproduced in test/scratch/RoundingProbe.t.sol (test_thirtyThreeWeiBuysPayZero, test_roundsDownPerBuy, test_previewBuyOverflows), all passing on this code, i.e. the behaviour described is what the code does.","severity":"low","snippet":"        return (grossAmount * TAX_BPS) / BPS_DENOMINATOR;","title":"Tax rounds down and truncates to zero below 34 wei; split buys always pay less than one buy of the same total"},{"citation":"resolved","description":"Numerical Gap seam 3 (boundary x invariant): v4 routers enforce amountOutMinimum against the swap's BalanceDelta, which is the gross amount the PoolManager is debited; the recipient's actual credit is value - tax (line 89). A buyer who sets amountOutMinimum = X can receive as little as 0.97X without the router reverting, and an exact-output swap for X delivers 0.97X. The README discloses this ('Routers that check output on the swap delta see the gross amount') and the quote path is a front-end concern; no on-chain fix exists for a transfer-level tax. Recorded as information so the judge has the number: slippage protection on MAD buys is effectively 3 percentage points looser than the user's parameter.","line":89,"path":"src/MADToken.sol","reproduction":"State: real PoolManager, MAD/PAIR pool at 1:1. Trader swaps exact-output amountSpecified = 100e18 MAD. BalanceDelta MAD leg == +100e18 (what a router compares to amountOutMinimum); token.balanceOf(trader) == 97e18; TAX_WALLET == 3e18. The tree's own test test_exactOutputBuyDeliversNetOfTax (test/MADTokenV4.t.sol:175) asserts these values.","severity":"info","snippet":"            super._update(from, to, value - tax);","title":"Exact-output buys and router minimum-output checks see the gross amount; the buyer receives 3% less than the delta a router protects"}],"hash":"7f4b567b9c3f67562b5b8abd2d007bf56d646150e8683c3cda035e1b435d803f","nodeId":"edf7d68e-96b3-4fb5-aec1-3fbc83916e70","outcome":"completed","summary":"The review is complete. `.imd-findings.json` holds five findings and nine coverage rows, with every snippet verified against its cited line, and no file outside the ignored `test/scratch/` folder was changed.\n\n**What the math itself does.** The tax formula, the two-leg split, the gross debit of the PoolManager and supply conservation all hold. I fuzzed conservation over the full supply range with sell-backs, checked the 34-wei rounding floor, confirmed the multiplication cannot overflow on reachable amounts, and traced the constructor and every launch flow. The defects are all at the boundary where \"sender is the PoolManager\" is used as the definition of a buy.\n\n**Findings, most severe first:**\n\n1. **Medium, claims bypass.** A trader who settles the MAD leg with ERC-6909 claims instead of an ERC-20 take never calls the token, so a full buy-and-sell round trip pays nothing to the tax wallet. Proven against the real PoolManager in a scratch test that fails on this code. The token alone cannot see claim mints, so this needs a requester decision.\n2. **Medium, sell leg untaxed.** The brief asks for a 3% buy/sell tax, and sells pay zero. The README's reasoning about v4 settlement is sound, so this is reported as an unmet requirement needing a scope decision, with the option of raising the buy rate to carry the intended round-trip total. Proof test included.\n3. **Low, liquidity withdrawal taxed.** Every ERC-20 exit from the PoolManager is charged, including LP withdrawals and fee collection. Disclosed, but the launch's 80% seed makes the possible amount large if that position is ever withdrawn to a non-exempt address.\n4. **Low, rounding.** The fee rounds down and truncates to zero below 34 wei. Dust-level and not profitably exploitable at 18 decimals, with the ceiling-division fix noted.\n5. **Info, slippage.** Router minimum-output checks see the gross delta, so buyers receive 3% less than their parameter protects. Disclosed in the README.\n\n**Coverage.** All three listed entry points have rows, plus six invariant and boundary rows. Nothing in my area was left unreached.","treeHash":null,"usage":{"cachedInputTokens":1290047,"inputTokens":418,"model":"claude-fable-5-1","outputTokens":26563,"runtime":"claude","turns":25,"wallClockMs":382711}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"f47f953f8e35921e","findings":[{"citation":"resolved","description":"The requester specified a 3% buy/sell tax paid to 0x1EE3ffadfE138415A2ddaedeEBe5F46616fA413d. _update only splits transfers whose sender is the PoolManager (line 83). A transfer whose recipient is the PoolManager (a sell) falls through to the plain ERC-20 update at line 92 and pays nothing, so half of the requested economic rule is not implemented. The README documents this as deliberate: on Uniswap v4 an input is credited by the PoolManager's balance change between sync and settle, so a token that shaves 3% off a transfer into the PoolManager leaves the swap 3% unsettled and the launch floor's trader (swap exact input, then settle) reverts with CurrencyNotSettled; charging the 3% on top of the amount is also refused by the floor because it sells the trader's whole bought balance. I confirmed both of those constraints against .imd/reads/protected/custom_token/Token.protected.t.sol (test_theSeedAndASwapEachWaySucceed sells `bought` with exact input and settles after). So this is a scope decision the requester must make, not a one-line fix: (a) accept buy-only at 3% as built, (b) keep the round trip at the intended 6% by taxing the buy leg at 600 bps, or (c) accept a sell tax on top of the amount that is only charged when the seller keeps a balance to pay it (evadable by selling everything). The author's README reasoning is sound; the finding is that the delivered rule differs from the brief and the requester has not visibly signed off on which option to take.","line":92,"path":"src/MADToken.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {MADToken} from \"src/MADToken.sol\";\n\n/// @notice The brief asks for a 3% buy/sell tax. A sell (transfer into the PoolManager) pays nothing.\ncontract SellUntaxedTest is Test {\n    address constant TAX_WALLET = 0x1EE3ffadfE138415A2ddaedeEBe5F46616fA413d;\n    address constant FACTORY = address(0xFAC7);\n    address constant POOL_MANAGER = address(0x900C);\n    address constant ALICE = address(0xA11CE);\n\n    MADToken token;\n\n    function setUp() public {\n        vm.prank(FACTORY);\n        token = new MADToken(FACTORY, POOL_MANAGER);\n        vm.prank(FACTORY);\n        token.transfer(ALICE, 1_000e18);\n    }\n\n    function test_sellOf1000MadPaysThreePercentTax() public {\n        vm.prank(ALICE);\n        token.transfer(POOL_MANAGER, 1_000e18);\n        // Buy side pays 30e18 on 1000e18; the brief asks for the same on the sell side.\n        assertEq(token.balanceOf(TAX_WALLET), 30e18, \"sell of 1000 MAD paid no 3% tax\");\n    }\n}","reproduction":"State: MADToken deployed by FACTORY=0xFAC7 with POOL_MANAGER=0x900C; FACTORY transfers 1000e18 MAD to ALICE. Call: ALICE.transfer(POOL_MANAGER, 1000e18). Actual: balanceOf(TAX_WALLET) == 0, balanceOf(POOL_MANAGER) == 1000e18. Expected under the brief (3% buy/sell): TAX_WALLET receives 30e18 on the sell, as it does on a 1000e18 buy. Reproduced with test/scratch/SellUntaxed.t.sol (fails: 0 != 30000000000000000000), and against the real v4 PoolManager in test/scratch/ClaimBypass.t.sol::test_sellPaysNoTax.","severity":"medium","snippet":"        super._update(from, to, value);","title":"Sell leg carries no tax: brief asks for a 3% buy/sell tax, only buys are taxed"},{"citation":"resolved","description":"The tax model equates a buy with an ERC-20 transfer out of the PoolManager. Uniswap v4 flash accounting lets a swap output be settled without any ERC-20 transfer: inside unlockCallback the trader calls PoolManager.mint(self, uint160(MAD), amount) to receive the MAD as ERC-6909 claim tokens, which zeroes the positive delta. MADToken._update never runs, so the 3% is never collected. The claims are freely transferable ERC-6909 balances inside the PoolManager, and a later sell is funded by PoolManager.burn(self, id, amount), again with no ERC-20 transfer. A trader (or any router that offers claim settlement) can therefore buy, hold, transfer and sell MAD with zero tax in either direction; the 3% is only paid by whoever finally takes ERC-20 MAD out of the PoolManager. The README's side-effects section does not mention this path. The token cannot close it by itself (it only observes ERC-20 transfers), and the launch pool's hook slot is taken by the floor's PoolInitializationGuard, so an afterSwap hook fee is not available in this launch. Minimal fix within the author's power: disclose in the README and the token NatSpec (lines 15-18) that the buy tax applies only to ERC-20 withdrawals from the PoolManager and is avoidable via ERC-6909 claims, so the requester can decide whether the rule as built is acceptable.","line":83,"path":"src/MADToken.sol","reproduction":"State (real v4 PoolManager, test/scratch/ClaimBypass.t.sol): MAD/PAIR pool at 1:1, fee 12500, tick spacing 60, liquidity 1_000_000e18 in [-600,600]; ClaimTrader holds 100e18 PAIR. Call 1: trader.trade(key, buy=true, 100e18): unlock -> swap(PAIR->MAD, exact input 100e18) -> manager.mint(trader, uint160(MAD), 98740249400371713293) -> sync/transfer/settle PAIR. Actual: manager.balanceOf(trader, id) == 98740249400371713293 MAD claims, token.balanceOf(trader) == 0, token.balanceOf(TAX_WALLET) == 0. Expected under the brief: TAX_WALLET += 2962207482011151398 (3% of the bought amount). Call 2: trader.trade(key, buy=false, 98740249400371713293): unlock -> manager.burn(trader, id, amount) -> swap(MAD->PAIR exact input) -> take PAIR. Actual: trader receives 97515745359112715272 PAIR, TAX_WALLET still 0. A full buy/sell round trip moved ~98.7 MAD through the pool and paid no tax. The test fails with 'buy settled as claims paid no tax: 0 != 2962207482011151398'.","severity":"medium","snippet":"        if (from == POOL_MANAGER && !isTaxExempt(to)) {","title":"Buy tax is bypassed by settling the MAD output as ERC-6909 claim tokens inside the PoolManager"},{"citation":"resolved","description":"The buy detector is 'sender is the PoolManager', but the PoolManager also transfers MAD out when a liquidity provider removes a position (the MAD leg of modifyLiquidity with negative liquidity, taken via take()) and when a seller who over-settled takes the surplus back. Neither is a purchase, yet 3% of the amount goes to the tax wallet. The README discloses the LP case under 'Side effects to be aware of'; I report it so the requester confirms that third-party LPs forfeiting 3% of their own MAD on withdrawal is intended, since that is value taken from a party who did not buy. The launch's own seed position is unaffected only because the factory is exempt as a recipient (isTaxExempt, line 68). No in-token fix exists that keeps the swap tax, because the token cannot tell a take() for a swap from a take() for a liquidity removal; the remedy is the requester's acceptance or a different tax design.","line":83,"path":"src/MADToken.sol","reproduction":"State (real v4 PoolManager, test/scratch/ClaimBypass.t.sol::test_lpRemovalIsTaxed): MAD/PAIR pool at 1:1; LiquidityRouter lp2 holds 1_000_000e18 MAD and PAIR. Call 1: lp2.modify(key, ModifyLiquidityParams(-600, 600, +100_000e18, 0)) deposits 2955301087913716968083 MAD into the position. Call 2: lp2.modify(key, ModifyLiquidityParams(-600, 600, -100_000e18, 0)) removes it; the MAD leg is taken from the PoolManager to lp2. Actual: lp2 receives 2866642055276305459040 MAD and TAX_WALLET receives 88659032637411509042 (3.0%). Expected for a withdrawal of one's own liquidity: lp2 receives the full 2955301087913716968083 and the tax wallet nothing. The repository's own test test/MADTokenV4.t.sol::test_removingLiquidityIsTreatedAsABuyOfTheTokenLeg asserts the same behaviour as intended.","severity":"low","snippet":"        if (from == POOL_MANAGER && !isTaxExempt(to)) {","title":"Every ERC-20 withdrawal from the PoolManager is taxed as a buy, including liquidity removal and settlement refunds"}],"hash":"a9c4e251d3cd4cc41b93f5c3d6b1f081d4b05e1c4d97acfdc3bb3abca1d2f165","nodeId":"b7b5208e-00eb-47d3-baa7-2df15e62e9e3","outcome":"completed","summary":"Review complete. The findings file is written with 3 findings and 11 coverage rows, and the scratch tests that back them fail on the current code as claimed.\n\n**What I found**\n\n- **Medium: the sell leg is untaxed.** The brief asks for a 3% buy/sell tax, but `_update` only splits transfers whose sender is the PoolManager. A seller moving 1000 MAD into the pool pays nothing where the brief expects 30 MAD. The README explains why: a deducted sell tax leaves a v4 swap unsettled and fails the launch floor's sell-everything check, which I confirmed against the protected test. So this is a requester decision, not a code slip. I laid out the options: accept buy-only, raise the buy leg to 6% to keep the round-trip total, or accept an evadable on-top sell tax.\n- **Medium: the buy tax is bypassable through ERC-6909 claims.** Against the real PoolManager, a trader who settles the swap output with `mint` instead of `take` received about 98.7 MAD as claim tokens and paid zero tax, then sold them back by burning claims, again with no tax. The token only sees ERC-20 transfers, and the launch's hook slot belongs to the floor's guard, so the author's remedy is disclosure and a requester sign-off.\n- **Low: every ERC-20 exit from the PoolManager is taxed as a buy.** A liquidity provider who deposited about 2955 MAD got back about 2867 and paid about 89 to the tax wallet on removal. The README discloses this. I flagged it so the requester confirms that LPs forfeiting 3% of their own MAD is intended.\n\n**What held**\n\nApprove, transferFrom mechanics, the constructor mint, supply conservation, all four launch flows arriving whole, the sell-everything settle, gross debiting of the pool on taxed buys, and the absence of any admin surface all traced clean. The vendored OpenZeppelin contracts are byte-identical to upstream v5.1.0. Static analysis leads were cosmetic and not reported.\n\n**Files**\n\n- `.imd-findings.json` holds the findings and coverage record.\n- `test/scratch/ClaimBypass.t.sol` and `test/scratch/SellUntaxed.t.sol` are the reproductions, embedded as proofs on the two medium findings. No tracked file was changed.","treeHash":null,"usage":{"cachedInputTokens":1393783,"inputTokens":546,"model":"claude-fable-5-1","outputTokens":30729,"runtime":"claude","turns":27,"wallClockMs":543596}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"fa2b7fcb5f53535a","findings":[],"hash":"b305db944d07e3010917489ee1129bbfa6254c874b402ae8fc3991d733a9b7d1","nodeId":"edf7d68e-96b3-4fb5-aec1-3fbc83916e70","outcome":"failed","summary":"Failed to authenticate: OAuth session expired and could not be refreshed","treeHash":null,"usage":{"cachedInputTokens":0,"inputTokens":0,"model":null,"outputTokens":0,"runtime":"claude","turns":1,"wallClockMs":1313}},{"artifacts":[],"attempt":1,"bundleHash":"8f35e2bbcbf39920b2f49857ad7567a815b81292285b13779e5e449468174fc8","device":"f009de0ce52c7b3a","findings":[],"hash":"d1eb1547984393eb37b228a711715d0db69f53d9af985ef11fb4c3dc96614978","nodeId":"df82e292-fd6b-4c96-93a2-7b8a1080cce9","outcome":"completed","summary":"Done. I wrote `launch.json` for the MAD custom token launch, and it is the only file changed.\n\n**What the manifest says**\n\n- Token contract `MADToken` with name \"Mutually Assured Destruction\", symbol MAD, 18 decimals.\n- Constructor arguments are `$factory` and `$poolManager`, matching the accepted constructor's two address parameters in declaration order. The token does not take a launch number, so none is listed.\n- Total supply is the constant the constructor mints to the factory, one billion tokens in minor units.\n- No application contracts. The accepted tree deploys the token alone.\n- Pool and economics are copied verbatim from the launch terms: FWA pair, fee 12500, tick spacing 60, the provenance-only initial price, and the requester's economics object.\n- Notes describe the 3% buy tax to the fixed tax wallet, the exemptions, the untaxed sell leg and the reason for it, and that no admin functions exist.\n\n**Verification**\n\n- `forge build` compiled cleanly and `forge test` with the verifier's fuzz seed passed all 36 tests.\n- A scratch script checked the manifest's keys, the constructor arity against the compiled ABI, the supply, the verbatim economics and pool objects, the contract name length, and the notes length.\n- `git status` shows only the new launch.json.\n\nOne point worth the requester's attention, already documented by the earlier builder in the README: the accepted token taxes buys only. A deducted sell tax cannot settle on Uniswap v4, so the manifest describes the build as it is rather than the brief's wording of \"buy/sell\".","treeHash":"c712a83a53677ce4314944de5b796d635c463bc3","usage":{"cachedInputTokens":241843,"inputTokens":162,"model":"claude-fable-5-1","outputTokens":3907,"runtime":"claude","turns":10,"wallClockMs":66731}}],"verification":[{"checks":[{"durationMs":2021,"exitCode":0,"name":"build","output":"Compiling 74 files with Solc 0.8.26\nSolc 0.8.26 finished in 1.93s\nCompiler run successful!\n","passed":true},{"durationMs":166,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 29 tests for test/MADToken.t.sol:MADTokenTest\n[PASS] testFuzz_buySplitsExactlyThreePercent(address,uint256) (runs: 256, μ: 203094, ~: 205160)\nLogs:\n  Bound result 687303679938600338952147778\n\n[PASS] testFuzz_nonPoolTransfersAreNeverTaxed(address,address,uint256) (runs: 256, μ: 151610, ~: 153308)\nLogs:\n  Bound result 1027401821803322773369\n\n[PASS] testFuzz_previewMatchesTransfer(address,uint256) (runs: 256, μ: 171451, ~: 174358)\nLogs:\n  Bound result 999999999999999999999999997\n\n[PASS] test_buyAbovePoolBalanceRevertsAtomically() (gas: 132900)\n[PASS] test_buyFromPoolPaysThreePercentToTaxWallet() (gas: 184581)\n[PASS] test_buyTaxRoundsDownAndTinyBuysAreUntaxed() (gas: 232832)\n[PASS] test_buyThenSellEverythingRoundTrip() (gas: 217906)\n[PASS] test_buyViaTransferFromIsTaxedTheSameWay() (gas: 204171)\n[PASS] test_constantsAndImmutables() (gas: 41790)\n[PASS] test_constructorEmitsMintTransfer() (gas: 6327)\n[PASS] test_constructorRejectsZeroFactory() (gas: 3836)\n[PASS] test_constructorRejectsZeroPoolManager() (gas: 3901)\n[PASS] test_deployScriptFunctionMintsToItsCaller() (gas: 1882119)\n[PASS] test_distributorClaimsArriveWhole() (gas: 135020)\n[PASS] test_factoryPayoutsArriveWhole() (gas: 219859)\n[PASS] test_isTaxExempt() (gas: 51351)\n[PASS] test_metadata() (gas: 32296)\n[PASS] test_mintsWholeSupplyToDeployerOnce() (gas: 39866)\n[PASS] test_noMintPauseOrFreezeFunctionsExist() (gas: 881899)\n[PASS] test_ownerViewDoesNotExist() (gas: 8362)\n[PASS] test_poolToExemptRecipientsIsUntaxed() (gas: 203219)\n[PASS] test_previewBuy() (gas: 28197)\n[PASS] test_sellToPoolArrivesWhole() (gas: 134821)\n[PASS] test_sellViaTransferFromArrivesWhole() (gas: 172585)\n[PASS] test_transferFromWithoutAllowanceReverts() (gas: 85972)\n[PASS] test_transferMoreThanBalanceReverts() (gas: 85780)\n[PASS] test_transferToZeroAddressReverts() (gas: 30774)\n[PASS] test_walletToWalletTransferFromIsUntaxed() (gas: 188109)\n[PASS] test_walletToWalletTransferIsUntaxed() (gas: 139587)\nSuite result: ok. 29 passed; 0 failed; 0 skipped; finished in 12.14ms (42.39ms CPU time)\n\nRan 7 tests for test/MADTokenV4.t.sol:MADTokenV4Test\n[PASS] testFuzz_buyThenSellAllAlwaysSettles(uint256) (runs: 256, μ: 954781, ~: 1208365)\nLogs:\n  Bound result 66311995527759753528262\n\n[PASS] test_buyPaysThreePercentOfTheGrossOutput() (gas: 337380)\n[PASS] test_exactOutputBuyDeliversNetOfTax() (gas: 308296)\n[PASS] test_removingLiquidityIsTreatedAsABuyOfTheTokenLeg() (gas: 178218)\n[PASS] test_seedingThePoolIsUntaxed() (gas: 33807)\n[PASS] test_sellOfWholeBoughtBalanceSettlesAndIsUntaxed() (gas: 505964)\n[PASS] test_supplyIsConservedAcrossTrades() (gas: 486845)\nSuite result: ok. 7 passed; 0 failed; 0 skipped; finished in 67.92ms (68.70ms CPU time)\n\nRan 2 test suites in 68.43ms (80.06ms CPU time): 36 tests passed, 0 failed, 0 skipped (36 total tests)\n","passed":true},{"durationMs":60,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"MADToken.approve(address,uint256)\",\"MADToken.transfer(address,uint256)\",\"MADToken.transferFrom(address,address,uint256)\"],\"files\":{\".gitignore\":5,\"README.md\":143,\"foundry.toml\":23,\"remappings.txt\":4,\"script/DeployMADToken.s.sol\":25,\"src/MADToken.sol\":98,\"test/MADToken.t.sol\":383,\"test/MADTokenV4.t.sol\":212},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true},{"durationMs":1158,"exitCode":0,"name":"slither","output":"slither: no results at low impact or above","passed":true},{"durationMs":562,"exitCode":0,"name":"aderyn","output":"[low] large-numeric-literal at src/MADToken.sol:31: Large Numeric Literal (2 places)\n[low] unused-public-function at src/MADToken.sol:74: Public Function Not Used Internally","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"287b5d74a8c599d4b1466ea367560b6b5ef11a09dc6637f6d85003fb1d5f74bd","verifiedTreeHash":"0fb8a0db32c92221d0ac97823e956234b2cd948f","verifierVersion":"0.1.0+ead9012e"},{"checks":[{"durationMs":3582,"exitCode":0,"name":"build","output":"Compiling 77 files with Solc 0.8.26\nSolc 0.8.26 finished in 3.43s\nCompiler run successful!\n","passed":true},{"durationMs":3255,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 29 tests for test/MADToken.t.sol:MADTokenTest\n[PASS] testFuzz_buySplitsExactlyThreePercent(address,uint256) (runs: 256, μ: 203458, ~: 205136)\nLogs:\n  Bound result 646849657513197297135251880\n\n[PASS] testFuzz_nonPoolTransfersAreNeverTaxed(address,address,uint256) (runs: 256, μ: 152239, ~: 153308)\nLogs:\n  Bound result 1000000000000000000000000000\n\n[PASS] testFuzz_previewMatchesTransfer(address,uint256) (runs: 256, μ: 171939, ~: 174346)\nLogs:\n  Bound result 770910657689822025738844622\n\n[PASS] test_buyAbovePoolBalanceRevertsAtomically() (gas: 132900)\n[PASS] test_buyFromPoolPaysThreePercentToTaxWallet() (gas: 184581)\n[PASS] test_buyTaxRoundsDownAndTinyBuysAreUntaxed() (gas: 232832)\n[PASS] test_buyThenSellEverythingRoundTrip() (gas: 217906)\n[PASS] test_buyViaTransferFromIsTaxedTheSameWay() (gas: 204171)\n[PASS] test_constantsAndImmutables() (gas: 41790)\n[PASS] test_constructorEmitsMintTransfer() (gas: 6327)\n[PASS] test_constructorRejectsZeroFactory() (gas: 3836)\n[PASS] test_constructorRejectsZeroPoolManager() (gas: 3901)\n[PASS] test_deployScriptFunctionMintsToItsCaller() (gas: 1882119)\n[PASS] test_distributorClaimsArriveWhole() (gas: 135020)\n[PASS] test_factoryPayoutsArriveWhole() (gas: 219859)\n[PASS] test_isTaxExempt() (gas: 51351)\n[PASS] test_metadata() (gas: 32296)\n[PASS] test_mintsWholeSupplyToDeployerOnce() (gas: 39866)\n[PASS] test_noMintPauseOrFreezeFunctionsExist() (gas: 881899)\n[PASS] test_ownerViewDoesNotExist() (gas: 8362)\n[PASS] test_poolToExemptRecipientsIsUntaxed() (gas: 203219)\n[PASS] test_previewBuy() (gas: 28197)\n[PASS] test_sellToPoolArrivesWhole() (gas: 134821)\n[PASS] test_sellViaTransferFromArrivesWhole() (gas: 172585)\n[PASS] test_transferFromWithoutAllowanceReverts() (gas: 85972)\n[PASS] test_transferMoreThanBalanceReverts() (gas: 85780)\n[PASS] test_transferToZeroAddressReverts() (gas: 30774)\n[PASS] test_walletToWalletTransferFromIsUntaxed() (gas: 188109)\n[PASS] test_walletToWalletTransferIsUntaxed() (gas: 139587)\nSuite result: ok. 29 passed; 0 failed; 0 skipped; finished in 34.67ms (82.11ms CPU time)\n\nRan 22 tests for test/MADTokenEdges.t.sol:MADTokenEdgesTest\n[PASS] testFuzz_buySplitHoldsOverTheWholeSupplyRange(uint256) (runs: 1000, μ: 161736, ~: 164040)\nLogs:\n  Bound result 5606\n\n[PASS] testFuzz_overdrawAlwaysRevertsAndLeavesNoTrace(uint256,uint256) (runs: 1000, μ: 134830, ~: 121048)\nLogs:\n  Bound result 468546798191946693043765466\n  Bound result 531453201808053307063711996\n\n[PASS] testFuzz_repeatedBuysNeverPayMoreThanOneBigBuy(uint256,uint256) (runs: 1000, μ: 191758, ~: 193623)\nLogs:\n  Bound result 495314950833750835459806677\n  Bound result 107477462\n\n[PASS] test_approveToTheZeroAddressReverts() (gas: 30825)\n[PASS] test_buyFromAnEmptyPoolReverts() (gas: 33741)\n[PASS] test_buyOfTheWholeSupply() (gas: 167543)\n[PASS] test_buyOneWeiAboveTheTaxThresholdOverThePoolReverts() (gas: 132717)\n[PASS] test_buyToTheZeroAddressRevertsWithoutPayingTax() (gas: 100984)\n[PASS] test_buyerSelfTransferIsUntaxed() (gas: 183321)\n[PASS] test_constructorRejectsBothZero() (gas: 3902)\n[PASS] test_infiniteAllowanceIsNotSpent() (gas: 126488)\n[PASS] test_oneWeiBuyIsUntaxed() (gas: 124223)\n[PASS] test_poolSelfTransferIsUntaxedAndChangesNothing() (gas: 104202)\n[PASS] test_previewBuyAtTheEdges() (gas: 45594)\n[PASS] test_previewBuyDoesNotOverflowAtLargeInputs() (gas: 9465)\n[PASS] test_routerBuyAboveTheAllowanceReverts() (gas: 150554)\n[PASS] test_routerBuyViaTransferFromSpendsTheGrossAllowance() (gas: 208997)\n[PASS] test_strangerCannotPullFromThePool() (gas: 85917)\n[PASS] test_supplyGoesToTheDeployerNotTheFactoryArgument() (gas: 26515)\n[PASS] test_taxWalletPayoutsAreUntaxed() (gas: 198420)\n[PASS] test_transferFromTheZeroAddressIsImpossible() (gas: 30762)\n[PASS] test_zeroValueBuyMovesNothingAndPaysNoTax() (gas: 120691)\nSuite result: ok. 22 passed; 0 failed; 0 skipped; finished in 82.65ms (152.39ms CPU time)\n\nRan 7 tests for test/MADTokenV4.t.sol:MADTokenV4Test\n[PASS] testFuzz_buyThenSellAllAlwaysSettles(uint256) (runs: 256, μ: 960584, ~: 1208365)\nLogs:\n  Bound result 2983964898557734684425\n\n[PASS] test_buyPaysThreePercentOfTheGrossOutput() (gas: 337380)\n[PASS] test_exactOutputBuyDeliversNetOfTax() (gas: 308296)\n[PASS] test_removingLiquidityIsTreatedAsABuyOfTheTokenLeg() (gas: 178218)\n[PASS] test_seedingThePoolIsUntaxed() (gas: 33807)\n[PASS] test_sellOfWholeBoughtBalanceSettlesAndIsUntaxed() (gas: 505964)\n[PASS] test_supplyIsConservedAcrossTrades() (gas: 486845)\nSuite result: ok. 7 passed; 0 failed; 0 skipped; finished in 86.67ms (87.51ms CPU time)\n\nRan 1 test for test/MADTokenV4Invariant.t.sol:MADTokenV4InvariantTest\n[PASS]\nMADTokenV4InvariantTest invariants:\n[PASS] invariant_everyBuyPaidThreePercent\n[PASS] invariant_everyHolderHoldsWhatTheLedgerSays\n[PASS] invariant_managerHoldsExactlyWhatItsAccountingSays\n[PASS] invariant_sellsAlwaysSettle\n[PASS] invariant_supplyIsFixedAndFullyAccounted\n MADTokenV4InvariantTest invariants (runs: 128, calls: 6400, reverts: 0)\n\n╭--------------+-----------------+-------+---------+----------╮\n| Contract     | Selector        | Calls | Reverts | Discards |\n+=============================================================+\n| MADV4Handler | addLiquidity    | 1034  | 0       | 0        |\n|--------------+-----------------+-------+---------+----------|\n| MADV4Handler | buyExactIn      | 1012  | 0       | 0        |\n|--------------+-----------------+-------+---------+----------|\n| MADV4Handler | buyExactOut     | 1058  | 0       | 0        |\n|--------------+-----------------+-------+---------+----------|\n| MADV4Handler | removeLiquidity | 1139  | 0       | 0        |\n|--------------+-----------------+-------+---------+----------|\n| MADV4Handler | sell            | 1114  | 0       | 0        |\n|--------------+-----------------+-------+---------+----------|\n| MADV4Handler | walletTransfer  | 1043  | 0       | 0        |\n╰--------------+-----------------+-------+---------+----------╯\n\nLogs:\n  Bound result 2827\n  Bound result 27034927320700424893247\n  Bound result 49999999999999999001692\n  Bound result 999999000000000010465\n  Bound result 90758\n  Bound result 49999999999999999002131\n  Bound result 2305\n  Bound result 30\n  Bound result 999999000000000000033\n  Bound result 1000000000000000\n  Bound result 18131\n  Bound result 49999999999999999010838\n  Bound result 0\n  Bound result 999999000000000012267\n  Bound result 100\n  Bound result 1260\n  Bound result 16484\n  Bound result 999999000000000053592\n  Bound result 50000000000000000000000\n  Bound result 643\n  Bound result 999999000000000010001\n  Bound result 49569\n  Bound result 1994\n  Bound result 799999999993231800\n  Bound result 999999000000000001016\n  Bound result 88403953666137\n  Bound result 0\n  Bound result 1719639407\n  Bound result 999999088403953666138\n  Bound result 33\n  Bound result 59969625251748\n  Bound result 999999999999999000\n  Bound result 49999999999999999012501\n  Bound result 250000000000000000000\n  Bound result 96970000000000000000\n  Bound result 1\n  Bound result 701406687\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 2.39s (2.38s CPU time)\n\nRan 1 test for test/MADTokenInvariant.t.sol:MADTokenInvariantTest\n[PASS]\nMADTokenInvariantTest invariants:\n[PASS] invariant_balancesSumToSupply\n[PASS] invariant_everyBuyPaidItsTax\n[PASS] invariant_poolIsDebitedGrossAndCreditedWhole\n[PASS] invariant_poolNeverPaysOutMoreThanItReceived\n[PASS] invariant_supplyIsFixed\n MADTokenInvariantTest invariants (runs: 256, calls: 15360, reverts: 0)\n\n╭-----------------+-----------------+-------+---------+----------╮\n| Contract        | Selector        | Calls | Reverts | Discards |\n+================================================================+\n| MADTokenHandler | approve         | 3757  | 0       | 0        |\n|-----------------+-----------------+-------+---------+----------|\n| MADTokenHandler | transfer        | 3912  | 0       | 0        |\n|-----------------+-----------------+-------+---------+----------|\n| MADTokenHandler | transferFrom    | 3902  | 0       | 0        |\n|-----------------+-----------------+-------+---------+----------|\n| MADTokenHandler | transferTooMuch | 3789  | 0       | 0        |\n╰-----------------+-----------------+-------+---------+----------╯\n\nLogs:\n  Bound result 64199\n  Bound result 0\n  Bound result 0\n  Bound result 3346\n  Bound result 0\n  Bound result 0\n  Bound result 38463463363218180947368932\n  Bound result 0\n  Bound result 6395\n  Bound result 0\n  Bound result 63400\n  Bound result 300\n  Bound result 0\n  Bound result 0\n  Bound result 8000\n  Bound result 27752611180335721755519986\n  Bound result 0\n  Bound result 100000000000000000000003343\n  Bound result 0\n  Bound result 0\n  Bound result 0\n  Bound result 0\n  Bound result 101000000000000000000\n  Bound result 7620\n  Bound result 12500\n  Bound result 999999999999999751\n  Bound result 0\n  Bound result 0\n  Bound result 5510\n  Bound result 0\n  Bound result 4594636\n  Bound result 288\n  Bound result 0\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 3.10s (3.10s CPU time)\n\nRan 5 test suites in 3.10s (5.69s CPU time): 60 tests passed, 0 failed, 0 skipped (60 total tests)\n","passed":true},{"durationMs":75,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"MADToken.approve(address,uint256)\",\"MADToken.transfer(address,uint256)\",\"MADToken.transferFrom(address,address,uint256)\"],\"files\":{\".gitignore\":5,\"README.md\":143,\"foundry.toml\":23,\"remappings.txt\":4,\"script/DeployMADToken.s.sol\":25,\"src/MADToken.sol\":98,\"test/MADToken.t.sol\":383,\"test/MADTokenEdges.t.sol\":253,\"test/MADTokenInvariant.t.sol\":218,\"test/MADTokenV4.t.sol\":212,\"test/MADTokenV4Invariant.t.sol\":333},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"47040b3a50a594abbd97356664e4edb4798507246d4cd033698d08b266e86a21","verifiedTreeHash":"49ef037e4e9c7cccf7c8efab979f9c561c69d49b","verifierVersion":"0.1.0+ead9012e"},{"checks":[{"durationMs":2162,"exitCode":0,"name":"build","output":"Compiling 74 files with Solc 0.8.26\nSolc 0.8.26 finished in 2.04s\nCompiler run successful!\n","passed":true},{"durationMs":224,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 29 tests for test/MADToken.t.sol:MADTokenTest\n[PASS] testFuzz_buySplitsExactlyThreePercent(address,uint256) (runs: 256, μ: 203094, ~: 205160)\nLogs:\n  Bound result 687303679938600338952147778\n\n[PASS] testFuzz_nonPoolTransfersAreNeverTaxed(address,address,uint256) (runs: 256, μ: 151610, ~: 153308)\nLogs:\n  Bound result 1027401821803322773369\n\n[PASS] testFuzz_previewMatchesTransfer(address,uint256) (runs: 256, μ: 171451, ~: 174358)\nLogs:\n  Bound result 2\n\n[PASS] test_buyAbovePoolBalanceRevertsAtomically() (gas: 132900)\n[PASS] test_buyFromPoolPaysThreePercentToTaxWallet() (gas: 184581)\n[PASS] test_buyTaxRoundsDownAndTinyBuysAreUntaxed() (gas: 232832)\n[PASS] test_buyThenSellEverythingRoundTrip() (gas: 217906)\n[PASS] test_buyViaTransferFromIsTaxedTheSameWay() (gas: 204171)\n[PASS] test_constantsAndImmutables() (gas: 41790)\n[PASS] test_constructorEmitsMintTransfer() (gas: 6327)\n[PASS] test_constructorRejectsZeroFactory() (gas: 3836)\n[PASS] test_constructorRejectsZeroPoolManager() (gas: 3901)\n[PASS] test_deployScriptFunctionMintsToItsCaller() (gas: 1882119)\n[PASS] test_distributorClaimsArriveWhole() (gas: 135020)\n[PASS] test_factoryPayoutsArriveWhole() (gas: 219859)\n[PASS] test_isTaxExempt() (gas: 51351)\n[PASS] test_metadata() (gas: 32296)\n[PASS] test_mintsWholeSupplyToDeployerOnce() (gas: 39866)\n[PASS] test_noMintPauseOrFreezeFunctionsExist() (gas: 881899)\n[PASS] test_ownerViewDoesNotExist() (gas: 8362)\n[PASS] test_poolToExemptRecipientsIsUntaxed() (gas: 203219)\n[PASS] test_previewBuy() (gas: 28197)\n[PASS] test_sellToPoolArrivesWhole() (gas: 134821)\n[PASS] test_sellViaTransferFromArrivesWhole() (gas: 172585)\n[PASS] test_transferFromWithoutAllowanceReverts() (gas: 85972)\n[PASS] test_transferMoreThanBalanceReverts() (gas: 85780)\n[PASS] test_transferToZeroAddressReverts() (gas: 30774)\n[PASS] test_walletToWalletTransferFromIsUntaxed() (gas: 188109)\n[PASS] test_walletToWalletTransferIsUntaxed() (gas: 139587)\nSuite result: ok. 29 passed; 0 failed; 0 skipped; finished in 14.75ms (37.09ms CPU time)\n\nRan 7 tests for test/MADTokenV4.t.sol:MADTokenV4Test\n[PASS] testFuzz_buyThenSellAllAlwaysSettles(uint256) (runs: 256, μ: 954781, ~: 1208365)\nLogs:\n  Bound result 1000000000002\n\n[PASS] test_buyPaysThreePercentOfTheGrossOutput() (gas: 337380)\n[PASS] test_exactOutputBuyDeliversNetOfTax() (gas: 308296)\n[PASS] test_removingLiquidityIsTreatedAsABuyOfTheTokenLeg() (gas: 178218)\n[PASS] test_seedingThePoolIsUntaxed() (gas: 33807)\n[PASS] test_sellOfWholeBoughtBalanceSettlesAndIsUntaxed() (gas: 505964)\n[PASS] test_supplyIsConservedAcrossTrades() (gas: 486845)\nSuite result: ok. 7 passed; 0 failed; 0 skipped; finished in 94.10ms (94.12ms CPU time)\n\nRan 2 test suites in 95.05ms (108.85ms CPU time): 36 tests passed, 0 failed, 0 skipped (36 total tests)\n","passed":true},{"durationMs":64,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"MADToken.approve(address,uint256)\",\"MADToken.transfer(address,uint256)\",\"MADToken.transferFrom(address,address,uint256)\"],\"files\":{\".gitignore\":5,\"README.md\":143,\"foundry.toml\":23,\"launch.json\":27,\"remappings.txt\":4,\"script/DeployMADToken.s.sol\":25,\"src/MADToken.sol\":98,\"test/MADToken.t.sol\":383,\"test/MADTokenV4.t.sol\":212},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"d1eb1547984393eb37b228a711715d0db69f53d9af985ef11fb4c3dc96614978","verifiedTreeHash":"c712a83a53677ce4314944de5b796d635c463bc3","verifierVersion":"0.1.0+ead9012e"}]}