Burrowsource, exactly what is deployedall files

test/BurrowHook.t.sol

// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;
import {
    BurrowHook,
    BurrowBaseHook,
    BurrowHooks,
    BurrowPoolKey,
    BurrowCurrency,
    BurrowSwapParams,
    BurrowBalanceDelta,
    BurrowBalanceDeltaLib,
    BurrowModifyLiquidityParams
} from "../src/BurrowHook.sol";
import {BurrowVaultLite} from "../src/BurrowVaultLite.sol";
import {BurrowValuer} from "../src/BurrowValuer.sol";
import {BurrowVaultFixture, BurrowTestBase, BurrowTestToken, BurrowSepoliaForkFixture, IERC20, IERC721} from "./Fixtures.t.sol";
import {BurrowTestFeeSink} from "./BurrowMarket.t.sol";

contract BurrowHookFactory {
    function deploy(bytes32 salt, address manager, address sqk, address vault, uint16 fee, address owner)
        external
        returns (BurrowHook)
    {
        return new BurrowHook{salt: salt}(manager, sqk, vault, fee, owner);
    }
}

abstract contract BurrowHookMining is BurrowTestBase {
    function _salt(BurrowHookFactory factory, address manager, address sqk, address sink, uint16 fee, address owner)
        internal
        pure
        returns (bytes32 salt, address expected)
    {
        bytes32 hash =
            keccak256(abi.encodePacked(type(BurrowHook).creationCode, abi.encode(manager, sqk, sink, fee, owner)));
        for (uint256 i;; i++) {
            salt = bytes32(i);
            expected =
                address(uint160(uint256(keccak256(abi.encodePacked(bytes1(0xff), address(factory), salt, hash)))));
            // Independently spell out the protocol mask, without using the hook's
            // own flag helper as the oracle for the CREATE2 address assertion.
            if (uint160(expected) & 0x3fff == 0x44) return (salt, expected);
        }
    }

    function _mineAndDeploy(address manager, address sqk, address sink, uint16 fee, address owner)
        internal
        returns (BurrowHook h)
    {
        BurrowHookFactory f = new BurrowHookFactory();
        (bytes32 salt, address expected) = _salt(f, manager, sqk, sink, fee, owner);
        h = f.deploy(salt, manager, sqk, sink, fee, owner);
        assertEq(address(h), expected, "CREATE2 address prediction");
        assertEq(uint256(uint160(address(h)) & 0x3fff), 0x44, "exactly afterSwap and afterSwapReturnDelta bits");
    }
}

contract BurrowHookMockManager {
    uint256 public totalTaken;
    address public lastCurrency;
    address public lastRecipient;
    uint256 public takes;

    function take(BurrowCurrency currency, address to, uint256 amount) external {
        lastCurrency = BurrowCurrency.unwrap(currency);
        lastRecipient = to;
        totalTaken += amount;
        takes++;
        IERC20(lastCurrency).transfer(to, amount);
    }

    function callAfterSwap(
        BurrowHook hook,
        BurrowPoolKey memory key,
        BurrowSwapParams memory params,
        BurrowBalanceDelta delta
    ) external returns (bytes4, int128) {
        return hook.afterSwap(msg.sender, key, params, delta, "");
    }
}

contract BurrowHookTest is BurrowVaultFixture, BurrowHookMining {
    BurrowHook internal hook;
    BurrowHookMockManager internal pool;
    BurrowPoolKey internal key;
    event FeeDonated(bytes32 indexed poolId, uint256 amount);
    event FeeHeld(bytes32 indexed poolId, uint256 amount);
    event Flushed(uint256 amount);

    function setUp() public {
        _setup(0);
        _seed(10000e18);
        pool = new BurrowHookMockManager();
        token.mint(address(pool), 1e30);
        hook = _mineAndDeploy(address(pool), address(token), address(vault), 100, address(this));
        key = BurrowPoolKey(
            BurrowCurrency.wrap(address(nft)), BurrowCurrency.wrap(address(token)), 3000, 60, address(hook)
        );
    }

    function _delta(int128 a0, int128 a1) internal pure returns (BurrowBalanceDelta) {
        return BurrowBalanceDelta.wrap((int256(a0) << 128) | int256(uint256(uint128(a1))));
    }

    function _params(bool zeroForOne) internal pure returns (BurrowSwapParams memory) {
        return BurrowSwapParams(zeroForOne, -100e18, 0);
    }

    function test_CREATE2AddressHasExactlyDeclaredPermissions() public view {
        BurrowHooks.Permissions memory p = hook.getHookPermissions();
        assertTrue(p.afterSwap && p.afterSwapReturnDelta, "required permissions");
        assertTrue(
            !p.beforeInitialize && !p.afterInitialize && !p.beforeAddLiquidity && !p.afterAddLiquidity
                && !p.beforeRemoveLiquidity && !p.afterRemoveLiquidity && !p.beforeSwap && !p.beforeDonate
                && !p.afterDonate && !p.beforeSwapReturnDelta && !p.afterAddLiquidityReturnDelta
                && !p.afterRemoveLiquidityReturnDelta,
            "undeclared flag enabled"
        );
        assertEq(hook.hookFlags(), 0x44, "declared flags");
        assertEq(uint160(address(hook)) & 0x3fff, 0x44, "actual address flags");
    }

    function test_ConstructorRejectsAnAddressWithWrongFlags() public {
        BurrowHookFactory f = new BurrowHookFactory();
        bytes32 hash = keccak256(
            abi.encodePacked(
                type(BurrowHook).creationCode,
                abi.encode(address(pool), address(token), address(vault), uint16(100), address(this))
            )
        );
        bytes32 salt;
        address expected = address(uint160(uint256(keccak256(abi.encodePacked(bytes1(0xff), address(f), salt, hash)))));
        if (uint160(expected) & 0x3fff == 0x44) {
            salt = bytes32(uint256(1));
            expected = address(uint160(uint256(keccak256(abi.encodePacked(bytes1(0xff), address(f), salt, hash)))));
        }
        assertTrue(uint160(expected) & 0x3fff != 0x44, "invalid deployment fixture");
        vm.expectRevert(abi.encodeWithSelector(BurrowHooks.HookAddressNotValid.selector, expected));
        f.deploy(salt, address(pool), address(token), address(vault), 100, address(this));
    }

    function test_SQKOutputSkimTakesAndDonatesToRealVault() public {
        uint256 assets = vault.totalAssets();
        uint256 supply = vault.totalSupply();
        vm.expectEmit(true, false, false, true, address(hook));
        emit FeeDonated(keccak256(abi.encode(key)), 1e18);
        (bytes4 selector, int128 delta) = pool.callAfterSwap(hook, key, _params(true), _delta(-110e18, 100e18));
        assertEq(uint32(selector), uint32(BurrowBaseHook.afterSwap.selector), "afterSwap selector");
        assertEq(uint256(uint128(delta)), 1e18, "positive unspecified currency delta");
        assertEq(pool.totalTaken(), 1e18, "fee taken from pool");
        assertEq(pool.lastCurrency(), address(token), "SQK currency");
        assertEq(pool.lastRecipient(), address(hook), "hook receives fee");
        assertEq(vault.totalAssets(), assets + 1e18, "donation reaches vault");
        assertEq(vault.totalSupply(), supply, "fee mints no shares");
        assertEq(token.balanceOf(address(hook)), 0, "all fees forwarded");
        assertEq(token.allowance(address(hook), address(vault)), 0, "donation consumes exact allowance");
    }

    function test_SQKAsCurrencyZeroUsesAmountZero() public {
        key.currency0 = BurrowCurrency.wrap(address(token));
        key.currency1 = BurrowCurrency.wrap(address(nft));
        (, int128 delta) = pool.callAfterSwap(hook, key, _params(false), _delta(75e18, -100e18));
        assertEq(uint256(uint128(delta)), 0.75e18, "currency zero output");
        assertEq(pool.totalTaken(), 0.75e18, "SQK amount zero fee");
    }

    function test_NonSQKOutputNonPositiveOutputAndDustTakeNothing() public {
        pool.callAfterSwap(hook, key, _params(false), _delta(100e18, -100e18));
        pool.callAfterSwap(hook, key, _params(true), _delta(-100e18, 0));
        pool.callAfterSwap(hook, key, _params(true), _delta(-100e18, -1));
        pool.callAfterSwap(hook, key, _params(true), _delta(-100e18, 99));
        assertEq(pool.takes(), 0, "no take without positive fee on SQK output");
        hook.setHookFeeBps(0);
        pool.callAfterSwap(hook, key, _params(true), _delta(-100e18, 100e18));
        assertEq(pool.takes(), 0, "zero fee no take");
    }

    /// forge-config: default.fuzz.runs = 1000
    function testFuzz_FeeBoundAndBalanceConservation(uint128 outputRaw, uint16 feeRaw, bool direction) public {
        uint256 amount = bound(outputRaw, 1, 1e27);
        uint16 fee = uint16(bound(feeRaw, 0, 100));
        hook.setHookFeeBps(fee);
        if (!direction) {
            key.currency0 = BurrowCurrency.wrap(address(token));
            key.currency1 = BurrowCurrency.wrap(address(nft));
        }
        uint256 expected = amount * fee / 10000;
        uint256 assets = vault.totalAssets();
        uint256 poolBalance = token.balanceOf(address(pool));
        (, int128 delta) = pool.callAfterSwap(
            hook,
            key,
            _params(direction),
            direction ? _delta(-1, int128(int256(amount))) : _delta(int128(int256(amount)), -1)
        );
        assertEq(uint256(uint128(delta)), expected, "floor fee delta");
        assertLe(expected, amount / 100, "one percent cap");
        assertEq(vault.totalAssets() - assets, expected, "fee to vault");
        assertEq(poolBalance - token.balanceOf(address(pool)), expected, "pool exactly debited");
        assertEq(token.balanceOf(address(hook)), 0, "no fee residue");
    }

    function test_OnlyPoolManagerMayInvokeAfterSwap() public {
        vm.expectRevert(BurrowBaseHook.NotPoolManager.selector);
        hook.afterSwap(address(this), key, _params(true), _delta(-1, 100e18), "");
        vm.expectRevert(BurrowBaseHook.NotPoolManager.selector);
        vm.prank(alice);
        hook.afterSwap(alice, key, _params(true), _delta(-1, 100e18), "");
    }

    function test_AllUnimplementedCallbacksRevert() public {
        BurrowModifyLiquidityParams memory lp = BurrowModifyLiquidityParams(-60, 60, 1, bytes32(0));
        BurrowBalanceDelta d = _delta(0, 0);
        vm.expectRevert(BurrowBaseHook.HookNotImplemented.selector);
        hook.beforeInitialize(alice, key, 1);
        vm.expectRevert(BurrowBaseHook.HookNotImplemented.selector);
        hook.afterInitialize(alice, key, 1, 0);
        vm.expectRevert(BurrowBaseHook.HookNotImplemented.selector);
        hook.beforeAddLiquidity(alice, key, lp, "");
        vm.expectRevert(BurrowBaseHook.HookNotImplemented.selector);
        hook.afterAddLiquidity(alice, key, lp, d, d, "");
        vm.expectRevert(BurrowBaseHook.HookNotImplemented.selector);
        hook.beforeRemoveLiquidity(alice, key, lp, "");
        vm.expectRevert(BurrowBaseHook.HookNotImplemented.selector);
        hook.afterRemoveLiquidity(alice, key, lp, d, d, "");
        vm.expectRevert(BurrowBaseHook.HookNotImplemented.selector);
        hook.beforeSwap(alice, key, _params(true), "");
        vm.expectRevert(BurrowBaseHook.HookNotImplemented.selector);
        hook.beforeDonate(alice, key, 1, 1, "");
        vm.expectRevert(BurrowBaseHook.HookNotImplemented.selector);
        hook.afterDonate(alice, key, 1, 1, "");
    }

    function test_RefusedDonationIsHeldThenAnyoneCanFlushIt() public {
        BurrowTestFeeSink sink = new BurrowTestFeeSink(token);
        BurrowHook h = _mineAndDeploy(address(pool), address(token), address(sink), 100, address(this));
        key.hooks = address(h);
        sink.setFail(true);
        vm.expectEmit(true, false, false, true, address(h));
        emit FeeHeld(keccak256(abi.encode(key)), 1e18);
        (, int128 delta) = pool.callAfterSwap(h, key, _params(true), _delta(-1, 100e18));
        assertEq(uint256(uint128(delta)), 1e18, "fee credit retained");
        assertEq(token.balanceOf(address(h)), 1e18, "held fee");
        assertEq(token.allowance(address(h), address(sink)), 0, "failed sink allowance revoked");
        vm.expectRevert(BurrowTestFeeSink.DonationRejected.selector);
        h.flush();
        assertEq(token.balanceOf(address(h)), 1e18, "failed flush preserves funds");
        sink.setFail(false);
        vm.expectEmit(false, false, false, true, address(h));
        emit Flushed(1e18);
        vm.prank(alice);
        h.flush();
        assertEq(token.balanceOf(address(h)), 0, "flushed all fees");
        assertEq(sink.donated(), 1e18, "fee delivered once");
        vm.expectRevert(BurrowHook.NothingToFlush.selector);
        h.flush();
    }

    function test_ApprovalFailureIsAtomicAndFlushCannotReenter() public {
        token.setRejectApproval(true);
        uint256 before = token.balanceOf(address(pool));
        vm.expectRevert(BurrowHook.ApproveFailed.selector);
        pool.callAfterSwap(hook, key, _params(true), _delta(-1, 100e18));
        assertEq(token.balanceOf(address(pool)), before, "failed approval leaked take");
        token.setRejectApproval(false);
        token.mint(address(hook), 1e18);
        token.setCallback(address(hook), abi.encodeWithSignature("flush()"));
        hook.flush();
        assertTrue(!token.callbackSucceeded(), "reentered flush");
        assertEq(
            keccak256(token.callbackResult()),
            keccak256(abi.encodeWithSignature("ReentrancyGuardReentrantCall()")),
            "guard failure"
        );
    }

    function test_ConstructorAndSetterCapsAndUnauthorizedOwnerActions() public {
        BurrowHookFactory f = new BurrowHookFactory();
        (bytes32 salt,) = _salt(f, address(pool), address(token), address(vault), 101, address(this));
        vm.expectRevert(abi.encodeWithSelector(BurrowHook.FeeTooHigh.selector, uint16(101), uint16(100)));
        f.deploy(salt, address(pool), address(token), address(vault), 101, address(this));
        for (uint256 i; i < 3; i++) {
            address manager_ = i == 0 ? address(0) : address(pool);
            address token_ = i == 1 ? address(0) : address(token);
            address vault_ = i == 2 ? address(0) : address(vault);
            (salt,) = _salt(f, manager_, token_, vault_, 0, address(this));
            vm.expectRevert(BurrowHook.ZeroAddress.selector);
            f.deploy(salt, manager_, token_, vault_, 0, address(this));
        }
        (salt,) = _salt(f, address(pool), address(token), address(vault), 0, address(0));
        vm.expectRevert(abi.encodeWithSignature("OwnableInvalidOwner(address)", address(0)));
        f.deploy(salt, address(pool), address(token), address(vault), 0, address(0));
        vm.expectRevert(abi.encodeWithSelector(BurrowHook.FeeTooHigh.selector, uint16(101), uint16(100)));
        hook.setHookFeeBps(101);
        vm.expectRevert(abi.encodeWithSignature("OwnableUnauthorizedAccount(address)", alice));
        vm.prank(alice);
        hook.setHookFeeBps(0);
        hook.transferOwnership(alice);
        vm.expectRevert(abi.encodeWithSignature("OwnableUnauthorizedAccount(address)", alice));
        vm.prank(alice);
        hook.setHookFeeBps(0);
        vm.prank(alice);
        hook.acceptOwnership();
        vm.prank(alice);
        hook.setHookFeeBps(0);
        assertEq(hook.hookFeeBps(), 0, "new owner can set capped fee");
    }
}

contract BurrowHookHandler is BurrowTestBase {
    BurrowHook public immutable hook;
    BurrowHookMockManager public immutable manager;
    BurrowTestToken public immutable token;
    BurrowTestFeeSink public immutable sink;
    uint256 public expectedFees;
    uint256 public unsolicited;
    bool public refusing;
    bool public badDelta;

    constructor(BurrowHook h, BurrowHookMockManager m, BurrowTestToken t, BurrowTestFeeSink s) {
        hook = h;
        manager = m;
        token = t;
        sink = s;
    }

    function acceptOwnership() external {
        hook.acceptOwnership();
    }

    function swap(uint128 raw, bool sqkOutput, bool direction) external {
        uint256 amount = bound(raw, 1, 1e24);
        uint256 charge = sqkOutput ? amount * hook.hookFeeBps() / 10000 : 0;
        address other = address(sink);
        BurrowPoolKey memory key = BurrowPoolKey(
            BurrowCurrency.wrap(direction ? other : address(token)),
            BurrowCurrency.wrap(direction ? address(token) : other),
            3000,
            60,
            address(hook)
        );
        if (!sqkOutput) direction = !direction;
        BurrowBalanceDelta d = BurrowBalanceDelta.wrap(
            direction
                ? (int256(-1) << 128) | int256(amount)
                : (int256(amount) << 128) | int256(uint256(type(uint128).max))
        );
        (, int128 delta) = manager.callAfterSwap(hook, key, BurrowSwapParams(direction, -1, 0), d);
        if (delta < 0 || uint256(uint128(delta)) != charge) badDelta = true;
        expectedFees += charge;
    }

    function setFee(uint16 raw) external {
        hook.setHookFeeBps(uint16(bound(raw, 0, 100)));
    }

    function refuse(bool value) external {
        refusing = value;
        sink.setFail(value);
    }

    function sendTokens(uint128 raw) external {
        uint256 amount = bound(raw, 1, 1e20);
        token.mint(address(hook), amount);
        unsolicited += amount;
    }

    function flush() external {
        if (!refusing && token.balanceOf(address(hook)) > 0) hook.flush();
    }
}

contract BurrowHookInvariantTest is BurrowHookMining {
    BurrowHook internal hook;
    BurrowHookMockManager internal manager;
    BurrowTestToken internal token;
    BurrowTestFeeSink internal sink;
    BurrowHookHandler internal handler;

    function setUp() public {
        token = new BurrowTestToken();
        manager = new BurrowHookMockManager();
        sink = new BurrowTestFeeSink(token);
        token.mint(address(manager), 1e30);
        hook = _mineAndDeploy(address(manager), address(token), address(sink), 100, address(this));
        handler = new BurrowHookHandler(hook, manager, token, sink);
        hook.transferOwnership(address(handler));
        handler.acceptOwnership();
    }

    struct FuzzSelector {
        address addr;
        bytes4[] selectors;
    }

    function targetContracts() public view returns (address[] memory a) {
        a = new address[](1);
        a[0] = address(handler);
    }

    function targetSelectors() public view returns (FuzzSelector[] memory a) {
        a = new FuzzSelector[](1);
        bytes4[] memory s = new bytes4[](5);
        s[0] = handler.swap.selector;
        s[1] = handler.setFee.selector;
        s[2] = handler.refuse.selector;
        s[3] = handler.sendTokens.selector;
        s[4] = handler.flush.selector;
        a[0] = FuzzSelector(address(handler), s);
    }

    /// forge-config: default.invariant.runs = 256
    /// forge-config: default.invariant.depth = 64
    /// forge-config: default.invariant.fail-on-revert = true
    function invariant_FeesAreDeliveredOrRecoverableAndNeverLost() public view {
        assertTrue(!handler.badDelta(), "fee delta incorrect");
        assertEq(manager.totalTaken(), handler.expectedFees(), "pool debits match skims");
        assertEq(
            token.balanceOf(address(hook)) + sink.donated(),
            handler.expectedFees() + handler.unsolicited(),
            "held plus donated fee conservation"
        );
        assertEq(token.balanceOf(address(sink)), sink.donated(), "sink backing");
        assertEq(token.allowance(address(hook), address(sink)), 0, "no residual sink approval");
        assertEq(token.balanceOf(address(manager)) + manager.totalTaken(), 1e30, "pool conservation");
        assertLe(hook.hookFeeBps(), 100, "fee cap");
    }
}

// Narrow, independently encoded v4 ABI. The fork router settles every delta in
// one unlock transaction, matching PoolManager's transient accounting lifetime.
interface BurrowRealPoolManager {
    function initialize(BurrowPoolKey memory key, uint160 sqrtPriceX96) external returns (int24);
    function unlock(bytes calldata data) external returns (bytes memory);
    function modifyLiquidity(
        BurrowPoolKey memory key,
        BurrowModifyLiquidityParams memory params,
        bytes calldata hookData
    ) external returns (BurrowBalanceDelta, BurrowBalanceDelta);
    function swap(BurrowPoolKey memory key, BurrowSwapParams memory params, bytes calldata hookData)
        external
        returns (BurrowBalanceDelta);
    function sync(BurrowCurrency currency) external;
    function settle() external payable returns (uint256);
    function take(BurrowCurrency currency, address to, uint256 amount) external;
}

contract BurrowForkSwapRouter {
    using BurrowBalanceDeltaLib for BurrowBalanceDelta;
    BurrowRealPoolManager public immutable manager;
    BurrowPoolKey private key;
    error NotManager();

    constructor(address m, BurrowPoolKey memory k) {
        manager = BurrowRealPoolManager(m);
        key = k;
    }

    function seed(uint128 liquidity) external {
        manager.unlock(abi.encode(uint8(0), uint256(liquidity), false));
    }

    function swap(uint256 amount, bool zeroForOne) external returns (BurrowBalanceDelta) {
        return abi.decode(manager.unlock(abi.encode(uint8(1), amount, zeroForOne)), (BurrowBalanceDelta));
    }

    function unlockCallback(bytes calldata data) external returns (bytes memory) {
        if (msg.sender != address(manager)) revert NotManager();
        (uint8 action, uint256 amount, bool zeroForOne) = abi.decode(data, (uint8, uint256, bool));
        BurrowBalanceDelta delta;
        if (action == 0) {
            (delta,) =
                manager.modifyLiquidity(key, BurrowModifyLiquidityParams(-600, 600, int256(amount), bytes32(0)), "");
        } else {
            delta = manager.swap(
                key,
                BurrowSwapParams(
                    zeroForOne,
                    -int256(amount),
                    zeroForOne ? uint160(4295128740) : uint160(1461446703485210103287273052203988822378723970341)
                ),
                ""
            );
        }
        _settle(key.currency0, delta.amount0());
        _settle(key.currency1, delta.amount1());
        return abi.encode(delta);
    }

    function _settle(BurrowCurrency currency, int128 amount) internal {
        if (amount < 0) {
            manager.sync(currency);
            IERC20(BurrowCurrency.unwrap(currency)).transfer(address(manager), uint256(-int256(amount)));
            manager.settle();
        } else if (amount > 0) {
            manager.take(currency, address(this), uint256(uint128(amount)));
        }
    }
}

contract BurrowHookSepoliaForkTest is BurrowSepoliaForkFixture, BurrowHookMining {
    function testFork_SepoliaRealPoolSwapSettlesFeeIntoVault() public {
        _fork();
        (BurrowVaultLite v,) = _liveVault();
        address lender = addr("fork hook lender");
        deal(SQK, lender, 10000e18);
        vm.startPrank(lender);
        IERC20(SQK).approve(address(v), type(uint256).max);
        v.deposit(10000e18, 0);
        vm.stopPrank();
        BurrowHook h = _mineAndDeploy(MANAGER, SQK, address(v), 100, address(this));
        BurrowTestToken other = new BurrowTestToken();
        bool sqkIsOne = SQK > address(other);
        BurrowPoolKey memory k = BurrowPoolKey(
            BurrowCurrency.wrap(sqkIsOne ? address(other) : SQK),
            BurrowCurrency.wrap(sqkIsOne ? SQK : address(other)),
            3000,
            60,
            address(h)
        );
        BurrowRealPoolManager(MANAGER).initialize(k, uint160(1 << 96));
        BurrowForkSwapRouter router = new BurrowForkSwapRouter(MANAGER, k);
        deal(SQK, address(router), 1e24);
        other.mint(address(router), 1e24);
        router.seed(1e24);
        uint256 assets = v.totalAssets();
        uint256 shares = v.totalSupply();
        uint256 buyerBefore = IERC20(SQK).balanceOf(address(router));
        BurrowBalanceDelta delta = router.swap(100e18, sqkIsOne);
        int128 received = sqkIsOne ? BurrowBalanceDeltaLib.amount1(delta) : BurrowBalanceDeltaLib.amount0(delta);
        uint256 fee = v.totalAssets() - assets;
        assertTrue(fee > 0, "real PoolManager paid SQK fee");
        assertTrue(received > 0, "swap paid SQK output");
        assertEq(fee, (uint256(uint128(received)) + fee) / 100, "fee is one percent of pre-hook output");
        assertEq(
            IERC20(SQK).balanceOf(address(router)) - buyerBefore,
            uint256(uint128(received)),
            "net swap delta reconciles"
        );
        assertEq(v.totalSupply(), shares, "hook fee does not mint");
        assertEq(IERC20(SQK).balanceOf(address(h)), 0, "real fee forwarded");
        assets = v.totalAssets();
        router.swap(50e18, !sqkIsOne);
        assertEq(v.totalAssets(), assets, "non-SQK output takes no fee");
    }
}

interface BurrowLivePositionActions {
    function nextTokenId() external view returns (uint256);
    function modifyLiquidities(bytes calldata unlockData, uint256 deadline) external payable;
    function permit2() external view returns (address);
}

interface BurrowLivePermit2 {
    function approve(address token, address spender, uint160 amount, uint48 expiration) external;
    function DOMAIN_SEPARATOR() external view returns (bytes32);
}