# Audit report

> Round 3, final check. TOP PRIORITY unchanged: the treasury (FeeVault) bank must never be split, spent, laid in the wall or moved, in any order of calls, including release/re-handover cycles. Verify the round-2 fixes: releaseVault clears activation and counters; every handover re-snapshots accounted and resets pot/pendingWall; activation requires vault.owner()==this and vault.strategy()==this; lowerAth observes first and caps lastMcap; in-swap slip = tickSpacing; wallTarget clamped to the usable tick range and an out-of-range liquidity is a no-op.

| | |
|---|---|
| Repository | https://github.com/ch3rryhq/drawdown-burn-audit.git |
| Commit | `14a22c460904bf6fa088c13d836833db5689ef9c` |
| Job | `2f1e1c5d-1d61-41d4-8018-6efd9105caf3` |
| Judged | 2026-10-16 00:43 UTC |
| Findings | 5 low · 3 info |

Four agents audited the code as it is at `14a22c4`, each in one area (math, permissions, economics, control flow),
and a judge reproduced, merged and ranked what they found, then read the code once more itself. Nothing in the repository was changed or deployed.

## Findings

### 1. Low: _build reverts with TickLiquidityOverflow instead of the documented no-op when the wall target is clamped to the floor of the tick range

`src/DrawdownBurnStrategy.sol:581`

```
        if (raw == 0 || raw > type(uint128).max) return;
```

Merged from audit_economics, audit_math and audit_flow (same mechanism, same fix). The round-2 fix clamps wallTarget() to the usable tick range and says a liquidity that does not fit is 'nothing, rather than a revert', but the guard only checks raw > type(uint128).max. Uniswap v4's Pool.modifyLiquidity enforces a far tighter bound, tickSpacingToMaxLiquidityPerTick(tickSpacing) = type(uint128).max / numTicks, about 1.15e34 for spacing 60. At the clamped floor range (lower = -887220, upper = -887220 + wallWidth) the sqrt-price difference is only ~1.3e8 (X96), so raw = pending * 2^96 / 1.3e8 ~ pending * 6e21. For any pending reserve roughly between 1.9e12 wei and 5.6e17 wei (up to ~0.56 ARB for width 600) raw passes the uint128 check and PoolManager.modifyLiquidity reverts with TickLiquidityOverflow(-887220), so the permissionless buildWall() and the owner's moveWall() revert for everyone until the next hour close lifts the ATH back to the market cap. Below ~1.9e12 wei a dust wall is actually laid at the floor; wallTarget() then returns that wall (the price can never cross below MIN_TICK), so every later buildWall tries to add to it and hits the same band, pinning pendingWall until the owner calls moveWall. Precondition is owner-only (lowerAth to a dust value is accepted, the only checks being mcap < athMcap and mcap != 0), so the bank is not at risk and the impact is a temporary, self-inflicted DoS of the wall path. Fix: after computing the liquidity also return when uint256(w.liquidity) + raw > Pool.tickSpacingToMaxLiquidityPerTick(tickSpacing) (import Pool from v4-core/src/libraries/Pool.sol), and/or floor lowerAth at a sane fraction of the current market cap.

**Reproduction**

Harness: real PoolManager, FeeHook (330 bps) at a mined hook address, Token 1e27 as currency0, MockERC20 reserve as currency1, spacing 60, pool initialized at tick 0 with full-range-ish liquidity 1e24 on [-6000, 6000]; strategy wallWidth 600, maxPerBuy 1e21, athMcap_ 0 (so ATH = mcap at tick 0); vault handed over and activated by a first buildWall() in a new block, pendingWall() == 0. Input: mint 1e15 wei of reserve to the vault and vault.sync (as the hook delivers a fee), roll one block, owner calls lowerAth(1): pendingWall() == 1e15 (tier 0), wall.liquidity == 0, wallTarget() == (-887220, -886620). Anyone calls buildWall(). Expected per the comment at lines 577-578: returns, pendingWall stays 1e15, no wall laid. Actual: reverts with TickLiquidityOverflow(-887220) from PoolManager.modifyLiquidity, because raw = 1e15 * 2^96 / 1.311e8 ~ 6.04e35 <= uint128.max passes the guard but exceeds maxLiquidityPerTick(60) ~ 1.151e34. Ran the attached proof on this tree: '[FAIL: TickLiquidityOverflow(-887220)] test_buildWallDoesNotRevertAtFloorRange()'. audit_flow additionally reports that with pendingWall = 1e12 a dust wall of liquidity 6.04e32 is laid at [-887220, -886620] and with 1e18 the call is the intended no-op, which brackets the band.

**Proof**: a Foundry test that fails on this code and passes once it is fixed.

```solidity
// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;

import {Test} from "forge-std/Test.sol";
import {PoolManager} from "v4-core/src/PoolManager.sol";
import {IHooks} from "v4-core/src/interfaces/IHooks.sol";
import {Hooks} from "v4-core/src/libraries/Hooks.sol";
import {PoolKey} from "v4-core/src/types/PoolKey.sol";
import {Currency} from "v4-core/src/types/Currency.sol";
import {TickMath} from "v4-core/src/libraries/TickMath.sol";
import {ModifyLiquidityParams} from "v4-core/src/types/PoolOperation.sol";
import {PoolModifyLiquidityTest} from "v4-core/src/test/PoolModifyLiquidityTest.sol";
import {MockERC20} from "solmate/src/test/utils/mocks/MockERC20.sol";

import {DrawdownBurnStrategy} from "src/DrawdownBurnStrategy.sol";
import {FeeVault} from "src/FeeVault.sol";
import {FeeHook} from "src/FeeHook.sol";
import {Token} from "src/Token.sol";
import {IFeeStrategy} from "src/interfaces/IFeeStrategy.sol";

/// _build() promises "nothing, rather than a revert" for a wall range it cannot lay, but it only
/// guards `raw > type(uint128).max`. Uniswap v4 caps liquidity per tick at
/// type(uint128).max / numTicks (~1.15e34 for spacing 60). With the wall target clamped to the
/// lowest usable range (an ATH lowered to dust), 0.001 ARB of pending reserve computes to
/// ~6.04e35 liquidity: under uint128.max, over the per-tick cap, so buildWall() reverts with
/// TickLiquidityOverflow. Fixed when _build returns instead of calling modifyLiquidity there.
contract WallFloorProofTest is Test {
    PoolManager pm;
    FeeVault vault;
    Token token;
    MockERC20 reserve;
    DrawdownBurnStrategy strat;
    PoolModifyLiquidityTest lp;
    PoolKey key;
    address owner = address(0xA11CE);

    function setUp() public {
        pm = new PoolManager(address(this));
        vault = new FeeVault(address(this));
        address reserveAddr = address(uint160(type(uint160).max) - 0xFFFF);
        deployCodeTo("MockERC20.sol:MockERC20", abi.encode("ARB", "ARB", uint8(18)), reserveAddr);
        reserve = MockERC20(reserveAddr);
        token = new Token("T", "T", 1e27, address(this));
        require(address(token) < reserveAddr, "order");

        address hookAddr = address(
            uint160(uint256(keccak256("hook")) & ~uint256(0x3FFF))
                | uint160(Hooks.AFTER_SWAP_FLAG | Hooks.AFTER_SWAP_RETURNS_DELTA_FLAG)
        );
        deployCodeTo("FeeHook.sol:FeeHook", abi.encode(pm, address(vault), reserveAddr, uint256(330), owner), hookAddr);

        key = PoolKey({
            currency0: Currency.wrap(address(token)),
            currency1: Currency.wrap(reserveAddr),
            fee: 3000,
            tickSpacing: 60,
            hooks: IHooks(hookAddr)
        });
        pm.initialize(key, TickMath.getSqrtPriceAtTick(0));

        lp = new PoolModifyLiquidityTest(pm);
        reserve.mint(address(this), 1e30);
        token.approve(address(lp), type(uint256).max);
        reserve.approve(address(lp), type(uint256).max);
        lp.modifyLiquidity(key, ModifyLiquidityParams(-6000, 6000, 1e24, 0), "");

        address[] memory ex = new address[](0);
        strat = new DrawdownBurnStrategy(pm, vault, key, token, reserveAddr, 0, ex, 1e21, 600, owner);
        vault.setStrategy(IFeeStrategy(address(strat)));
        vault.transferOwnership(address(strat));
        reserve.mint(address(vault), 100e18); // the bank
        vm.roll(block.number + 1);
        strat.buildWall(); // first reading after handover: activates and counts the bank
        assertEq(strat.pendingWall(), 0);
    }

    function test_buildWallDoesNotRevertAtFloorRange() public {
        // 0.001 ARB of fee arrives, as the hook delivers it: transfer then sync.
        reserve.mint(address(vault), 1e15);
        vault.sync(address(reserve));
        vm.roll(block.number + 1);

        vm.prank(owner);
        strat.lowerAth(1); // observes first: the fee is split (tier 0, all to the wall), then the ATH is floored
        assertEq(strat.pendingWall(), 1e15);
        (,, uint128 liq) = strat.wall();
        assertEq(liq, 0);

        (int24 lower, int24 upper) = strat.wallTarget();
        assertEq(lower, -887220); // clamped to the lowest usable tick: correct
        assertEq(upper, -887220 + 600);

        // Documented as a no-op. Actual: reverts with TickLiquidityOverflow(-887220).
        strat.buildWall();

        // Once it does not revert, the pending reserve must still be intact and no wall laid.
        assertEq(strat.pendingWall(), 1e15);
        (,, liq) = strat.wall();
        assertEq(liq, 0);
        assertEq(reserve.balanceOf(address(vault)), 100e18 + 1e15);
    }
}
```

### 2. Low: lowerAth lowers athMcap and caps lastMcap but leaves the block's anchored tier: fees for the rest of the L1 block split at the old tier

`src/DrawdownBurnStrategy.sol:715`

```
        athMcap = mcap;
```

Merged from audit_economics and audit_permissions (same mechanism, same fix). _observe writes anchor.burnBps once per L1 block from _tierBps(mcap, ath) (lines 437-439). lowerAth calls _observe first, which anchors the block and stores the tier for the OLD ATH, then lowers athMcap and caps lastMcap but never recomputes anchor.burnBps. Every fee that arrives in the remainder of that L1 block (about 12 s on Arbitrum, many L2 blocks and swaps) is split at line 474 by the stale, higher tier: more into the pot (spent on buybacks 'whatever the tier later reads') and less into the wall than the ATH the owner just set implies, and currentBurnBps() disagrees with wallTarget(), which already uses the new ATH. Bounded to one L1 block of fees and only moves reserve between pot and pendingWall, never the bank. Fix: after lowering, write anchor.burnBps = uint16(_tierBps(lastMcap, mcap)) (lastMcap is the block's anchored reading once capped).

**Reproduction**

Hookless v4 pool (spacing 60) initialized at tick -69000, Token 1e27 as currency0, MockERC20 as currency1. Deploy a probe strategy with athMcap_ = 0 to read mcap = lastMcap(), then the strategy under test with athMcap_ = 2 * mcap (50% under the ATH). Roll one block and warp 13 s: currentBurnBps() == 7500. Owner calls lowerAth(mcap * 11 / 10). Expected: athMcap == mcap*11/10 (holds) and this block's tier is 2500, since tierBps(lastMcap(), athMcap()) == 2500 (holds). Actual: currentBurnBps() still returns 7500 for the rest of the block, and a fee arriving now is split 75% to the pot. Ran the attached proof on this tree: '[FAIL: the block's anchored tier must follow the lowered ATH: 7500 != 2500] test_lowerAthUpdatesTheBlocksTier()'.

**Proof**: a Foundry test that fails on this code and passes once it is fixed.

```solidity
// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;

import {Test} from "forge-std/Test.sol";
import {PoolManager} from "v4-core/src/PoolManager.sol";
import {IPoolManager} from "v4-core/src/interfaces/IPoolManager.sol";
import {IHooks} from "v4-core/src/interfaces/IHooks.sol";
import {PoolKey} from "v4-core/src/types/PoolKey.sol";
import {Currency} from "v4-core/src/types/Currency.sol";
import {TickMath} from "v4-core/src/libraries/TickMath.sol";
import {MockERC20} from "solmate/src/test/utils/mocks/MockERC20.sol";
import {FeeVault} from "src/FeeVault.sol";
import {Token} from "src/Token.sol";
import {DrawdownBurnStrategy} from "src/DrawdownBurnStrategy.sol";

/// lowerAth lowers athMcap and caps lastMcap but leaves the block's anchored tier (anchor.burnBps)
/// at the value computed for the old ATH, so fees arriving in the rest of the L1 block are split
/// by the stale tier and currentBurnBps() disagrees with the ATH the owner just set.
contract LowerAthTierTest is Test {
    PoolManager pm;
    MockERC20 arb;
    Token token;
    FeeVault vault;
    PoolKey key;

    function setUp() public {
        pm = new PoolManager(address(this));
        arb = new MockERC20("ARB", "ARB", 18);
        for (uint256 i = 0; i < 64; i++) {
            Token t = new Token{salt: bytes32(i)}("T", "T", 1e27, address(this));
            if (address(t) < address(arb)) {
                token = t;
                break;
            }
        }
        require(address(token) != address(0), "ordering");
        vault = new FeeVault(address(this));
        // No hook is needed to read the tier: the pool only has to exist at a price.
        key = PoolKey({
            currency0: Currency.wrap(address(token)),
            currency1: Currency.wrap(address(arb)),
            fee: 3000,
            tickSpacing: 60,
            hooks: IHooks(address(0))
        });
        pm.initialize(key, TickMath.getSqrtPriceAtTick(-69000));
    }

    function test_lowerAthUpdatesTheBlocksTier() public {
        address[] memory ex = new address[](0);
        // Deploy with the ATH at twice the market cap: 50% under it, tier 7500.
        DrawdownBurnStrategy probe = new DrawdownBurnStrategy(
            IPoolManager(address(pm)), vault, key, token, address(arb), 0, ex, 1e21, 600, address(this)
        );
        uint256 mcap = probe.lastMcap();
        DrawdownBurnStrategy strat = new DrawdownBurnStrategy(
            IPoolManager(address(pm)), vault, key, token, address(arb), mcap * 2, ex, 1e21, 600, address(this)
        );
        vm.roll(block.number + 1);
        vm.warp(block.timestamp + 13);
        assertEq(strat.currentBurnBps(), 7500, "50% under the ATH");

        // The owner lowers the ATH to 10% above the market cap: the tier for this block should be 2500.
        strat.lowerAth(mcap * 11 / 10);
        assertEq(strat.athMcap(), mcap * 11 / 10);
        assertEq(strat.tierBps(strat.lastMcap(), strat.athMcap()), 2500, "the tier the new ATH implies");
        assertEq(strat.currentBurnBps(), 2500, "the block's anchored tier must follow the lowered ATH");
    }
}
```

### 3. Low: After releaseVault, moveWall reverts whenever the wall can be re-laid, and burnWall's pot credit silently becomes bank at the next handover

`src/DrawdownBurnStrategy.sol:762`

```
    ///         stays with this contract until {withdrawWall}, {burnWall} or {moveWall}.
```

Merged from audit_economics, audit_flow and audit_permissions (same released-state mechanism). releaseVault's natspec (lines 761-762) names withdrawWall, burnWall and moveWall as the ways to get the wall out afterwards, but only withdrawWall behaves as documented once the vault is owned by the EOA. moveWall runs _takeDownWall (reserve back to the vault via poolManager.take + vault.sync, which needs no ownership, _pendingWall += R) and then _build; whenever the price and the anchor are at or above the new target, _build calls _payFromVault (line 591) -> vault.withdraw (line 808), which is onlyOwner on FeeVault, so the whole call reverts with OwnableUnauthorizedAccount(strategy) and the wall cannot be moved while released. When the price is under the new target, moveWall 'succeeds' but only parks R as pendingWall, which the next activation resets to 1 (line 464), turning R into bank. burnWall succeeds and sets _pot = 1 + R, but buyback()/buyInside then revert at vault.withdraw and the next activation resets _pot, so R becomes bank instead of being 'bought back and burned, whatever the tier' as lines 721-722 promise. The same _payFromVault path makes a permissionless buildWall revert while released once a crossed-wall removal has credited reserve fees to _pendingWall. Conservative for the bank (it can only grow) and no funds are lost (withdrawWall and withdrawFromVault still work), but two of the three documented released-state exits do not do what they say. Fix: in _build (or moveWall/burnWall) revert early with a clear error, or return leaving the reserve pending, when vault.owner() != address(this); and document that burnWall's pot credit does not survive a handover.

**Reproduction**

Same harness as finding 1 (real PoolManager, FeeHook, Token, MockERC20 reserve, spacing 60, tick 0, strategy active with the vault, 100e18 bank). Mint 10e18 of reserve to the vault and sync, roll, buildWall(): a wall is laid at wallTarget() with liquidity > 0 and pendingWall() == 0. Owner calls releaseVault(): vault.owner() == owner EOA. Roll one block, owner calls moveWall(). Expected (natspec): the wall is taken down and laid again under the current ATH. Actual: revert OwnableUnauthorizedAccount(address(strategy)) from FeeVault.withdraw inside _build -> _payFromVault (test_moveWall_after_release_reverts passes with that expectRevert). Second sequence from the same state: owner calls burnWall() instead: it succeeds, pot() > 9e18; buyback() in the next block reverts with OwnableUnauthorizedAccount; the owner then calls vault.transferOwnership(strategy) and anyone calls buyback() in a new block: pot() == 0 and the vault holds 100e18 + the wall's reserve as uncounted bank (test_burnWall_after_release_becomes_bank).

### 4. Low: withdrawWall's 'as bank' marking is local to one instance: a successor DrawdownBurnStrategy splits a retired instance's returned wall reserve as a fee

`src/DrawdownBurnStrategy.sol:816`

```
        accounted += vault.sync(reserve);
```

From audit_flow. _toVault returns reserve to the vault with poolManager.take + vault.sync and bumps this instance's own `accounted` by the synced amount so its next _observe does not read it as fresh. That marking lives only in the calling instance. The documented upgrade path is releaseVault then installing another strategy; if the owner installs a second DrawdownBurnStrategy B and B activates (snapshotting accounted = vault.totalReceived) before the retired instance A has exited its wall, then A.withdrawWall(), documented as returning the reserve 'as bank, never split, spent or laid again', raises vault.totalReceived by the wall's reserve while B.accounted is unchanged, so B's next _observe splits the whole amount into B's pot/pendingWall and B spends it on buybacks or lays it as a wall. The same holds for A.burnWall() and A.moveWall(). The pre-handover treasury is not reached: B's bank snapshot excludes it and B only ever spends increases of totalReceived after its activation, so this is a violation of withdrawWall's documented guarantee, not of the top-priority bank invariant. Fix options that keep the design: on an instance that no longer owns the vault, have _withdrawWall send the reserve to owner() instead of the vault, or revert and require the wall exits to run before the successor activates; at minimum document that every wall exit on a retired instance must precede the successor's activation.

**Reproduction**

Same harness. Strategy A active, 10e18 of reserve credited and laid as a wall by buildWall(). Owner: A.releaseVault(); deploy B = new DrawdownBurnStrategy(same arguments); vault.setStrategy(B); vault.transferOwnership(B); next block B.buyback() (B activates, B.pot() + B.pendingWall() == 0). Next block owner calls A.withdrawWall(): vault.totalReceived(reserve) rises by r > 9e18. Next block B.buyback(). Expected per withdrawWall's natspec: the amount is bank and B.pot() + B.pendingWall() stays 0. Actual: B.pot() + B.pendingWall() == r (all of it to B's pendingWall at tier 0): test_withdrawWall_counted_as_fee_by_new_strategy asserts exactly this on the current code.

### 5. Low: Constructor does not check that the pool behind the key is initialized: a mismatched fee/tickSpacing/hooks deploys a strategy anchored at tick 0 with the circulating supply as its ATH

`src/DrawdownBurnStrategy.sol:253`

```
        (, int24 tick,,) = poolManager_.getSlot0(key.toId());
```

From audit_permissions. The constructor validates only currency0 == token and currency1 == reserve (lines 231-233); fee, tickSpacing and hooks are stored unchecked. For a key whose pool was never initialized, getSlot0 returns (0, 0, 0, 0): tick 0 is a price of 1 reserve per token, so marketCapAt(0) == circulatingSupply(), which becomes the ATH whenever it exceeds athMcap_. Deployment succeeds, the vault can be handed over and activation succeeds (it touches only the vault), every fee is then split at 100% burn (any real market cap is far more than 60% under an ATH of ~1e27) and every poolManager.swap / modifyLiquidity against the stored key reverts with PoolNotInitialized: buyInside fails inside the vault's try/catch on every trade (BuybackSkipped), buyback() and buildWall() with a pending wall revert. Recovery is a redeploy plus a second handover. The bank is never spent. Deployment-time input validation, low. Fix: read sqrtPriceX96 from getSlot0 and revert PoolMismatch() when it is 0; optionally require key.hooks == the known FeeHook.

**Reproduction**

Same harness, live pool at (token, reserve, fee 3000, spacing 60, FeeHook). Deploy new DrawdownBurnStrategy(pm, vault, keyWithFee500, token, reserve, 0, [], 1e21, 600, owner) where keyWithFee500 is the same key with fee = 500 (no such pool). Expected: the constructor reverts (PoolMismatch). Actual: it deploys, athMcap() == circulatingSupply() (1e27, tick 0 read from an empty slot0) and anchor.tick == 0 while the real pool is elsewhere: test_constructor_accepts_uninitialized_pool asserts this on the current code.

### 6. Info: wallTarget() reverts on mulDiv overflow when the circulating supply is tiny but non-zero (level >= circ * 2^64)

`src/DrawdownBurnStrategy.sol:405`

```
        uint256 sqrtP = circ == 0 ? TickMath.MAX_SQRT_PRICE - 1 : Math.sqrt(FullMath.mulDiv(level, 1 << 192, circ));
```

From audit_math. FullMath.mulDiv(level, 1 << 192, circ) reverts when the quotient does not fit 256 bits, i.e. when level >= circ * 2^64. The circ == 0 branch is handled and the sqrt price is clamped on both ends afterwards, but the clamp never runs for a circulating supply that is tiny yet non-zero (with an ATH of 1e24, any circ <= ~2.17e4 wei). buildWall and moveWall call wallTarget() via _build, so both revert in that state; burnWall, withdrawWall, buyback and releaseVault are unaffected and the bank is not at risk. Reaching it needs almost the entire supply burned or held by the excluded addresses, so it is an edge for completeness. Fix: clamp before dividing, e.g. if (circ == 0 || level / circ >= (1 << 64)) sqrtP = TickMath.MAX_SQRT_PRICE - 1; else sqrtP = Math.sqrt(mulDiv(...)).

**Reproduction**

Hookless pool at tick 0 for a fresh Token (1e27) against the reserve; the deployer holds all but 1e4 wei, which sit at 0xBEEF; deploy the strategy with excluded_ = [deployer] and athMcap_ = 1e24: circulatingSupply() == 1e4, athMcap() == 1e24. Call wallTarget(). Expected: the price is above MAX_SQRT_PRICE, so the target is clamped to the top usable range (887220 - 600, 887220). Actual: level = 4e23 and 4e23 * 2^192 / 1e4 ~ 2^257 overflows, FullMath.mulDiv reverts, and wallTarget()/buildWall()/moveWall() revert. After 0xBEEF burns the 1e4 wei (circ == 0) wallTarget() returns (886620, 887220) as intended: test_wallTarget_overflow_tiny_circ asserts both on the current code.

### 7. Info: Handover order is not enforced: transferring vault ownership to the strategy before setStrategy leaves it inactive, with releaseVault as the only way back

`src/DrawdownBurnStrategy.sol:459`

```
            if (vault.owner() != address(this) || address(vault.strategy()) != address(this)) return;
```

From audit_permissions. Activation correctly requires both vault.owner() == this and vault.strategy() == this (the round-2 fix is in place). The strategy exposes no passthrough to FeeVault.setStrategy, so once the vault's ownership has been transferred to the strategy only the strategy could call setStrategy, and it never does. If the owner runs the handover as transferOwnership(strategy) then setStrategy(strategy), the second call reverts and the strategy stays inactive: fees keep accruing to the vault as bank and are never split, with no Activated event to show it. Recovery exists (strategy.releaseVault(), then setStrategy, then transferOwnership again) and no funds are at risk, but a deploy script that gets the order wrong fails silently. Fix (optional): an onlyOwner claimVault() on the strategy that calls vault.setStrategy(this) when vault.owner() == this, or a note in the README's handover procedure that setStrategy comes first.

**Reproduction**

Same harness. Owner calls strategy.releaseVault() (vault back to the EOA), deploys B, then calls vault.transferOwnership(B) followed by vault.setStrategy(B). Expected: B activates on the next call. Actual: setStrategy reverts with OwnableUnauthorizedAccount(owner); in the next block B.buyback() returns at line 459 and B.anchor().active stays false: test_handover_order_not_enforced asserts this on the current code.

### 8. Info: Dead code: `to` decoded from unlock data is never used (compiler warning), and LiquidityAmounts is imported but unused

`src/DrawdownBurnStrategy.sol:326`

```
        (uint8 action, address to) = abi.decode(data, (uint8, address));
```

From audit_flow. Every unlock caller encodes address(0) as the second word and unlockCallback never reads `to`; solc warns 'Unused local variable' at 326:24 on every build. The LiquidityAmounts import at line 21 is likewise unused. Neither affects behaviour; dropping both silences the warning and shrinks the audited surface. If `to` is meant for a future recipient-bearing action it must be validated before use.

**Reproduction**

forge build on this tree prints 'Warning (2072): Unused local variable. --> src/DrawdownBurnStrategy.sol:326:24'. grep LiquidityAmounts src/DrawdownBurnStrategy.sol matches only the import on line 21.

---

Judge's submission `6d40e632c379b4580598c8d58c25582f2a042bcf39583383a8dbf7c31a8cccb6`, accepted on the IdentityMD network. Acceptance means the report met the job's checks;
it is not a guarantee that the code has no other defects.
