# Audit report

> Audit BOSS before its launch: BossHook (Uniswap v4 hook, 2.5% fee in IMD on top of the pool LP fee of 1.25%: 1.5% to the pot, 1% to the treasury; every buy hits the boss, every sell heals it; on a kill 10% of the pot seeds the next boss and the rest is split 50/50 between the killing blow and, by damage, the wallets by the damage each still holds; credit follows the BOSS: BossToken reports every transfer to the hook, a buy is credited to the wallet that receives the BOSS from the pool, BOSS handed back to the pool forfeits in proportion, BOSS sent to another wallet carries its credit, the killing blow travels with its BOSS and the prize stays with its largest holder; within one transaction a sell takes back the pending credit of the BOSS it hands in, the net credit created never exceeds the HP removed, and pending credit is scoped to the swap's sender), AbilityBoard and OracleGate (abilities written by the swarm, accepted only with an IMD oracle attestation, 21 seats, quorum 15). This commit fixes the findings of job be67d693; the README lists the known limits. Focus: can anyone drain or lock the pot, steal another wallet's prize or credit, get credit without net damage, block trades or transfers, replay an attestation, or make the treasury pay more than intended? Every finding with a location, a reproduction and a Foundry proof where possible.

| | |
|---|---|
| Repository | https://github.com/HitTheBossApp/boss.git |
| Commit | `54e917cf434d44c422ae795baf57fa66174c36b7` |
| Job | `853ff4df-cb75-4375-8c73-475ff7c5c998` |
| Judged | 2026-10-13 00:31 UTC |
| Findings | 1 medium · 3 low · 1 info |

Four agents audited the code as it is at `54e917c`, 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. Medium: Closed-round credit of any bystander moves with their BOSS inside the transaction that closed the round (stranded on a custodying router, or handed to a payee)

`src/BossHook.sol:714`

```
        if (!sink && !damageClaimed[kr][from]) {
```

Once a round closes its credit is meant to be frozen: in any later transaction `onBossTransfer` returns before the closed-round block (T_KILL_ROUND is zero) and the README says a closed round is never forfeited. The exception that lets the killing blow travel with its BOSS is keyed only on `T_KILL_ROUND != 0`, so inside the transaction that closed round kr every later hop of ANY wallet's round-kr BOSS runs `_releaseClosed(kr, from, value)` and moves the proportional share of that wallet's closed-round damage to the hop's recipient; only the signer's undelivered blow is excluded. In a multi-party transaction (an ERC-4337 bundle, a relayer multicall, a solver batch) a player whose operation runs after someone else's kill therefore loses a share of their round-kr claim: (1) a sell through a custodying router (Universal Router + Permit2: wallet -> router is a hop that moves the credit onto the router, router -> PoolManager is a sink that leaves it there) strands the credit on the router, where `claimDamage(kr, router)` is permissionless and pays IMD to the router, which anyone can SWEEP out of the Universal Router; (2) a plain transfer to another wallet hands the payee the share. The same operation one transaction later moves nothing, so a player's payout depends on what the bundler packed before it, and a bundler that lands its own kill op in front of a victim's queued sell op collects the victim's damage share. The damage pool total is conserved (eligibleDamage unchanged), so this is redistribution from the bystander to the router or payee, not inflation. Not among the README's known limits (it documents only that the killing blow travels). Two specialist reports (economics: transfer to another wallet; flow: sell through a custodying router) describe this one mechanism and are merged here. Fix: restrict closed-round travel to the killing blow's credit, e.g. on a hop move closed-round damage only for BOSS the hook tracks as blow BOSS held by `from` (the T_KILL_HELD count that `_trackKill` already keeps, slicing the kill credit that arrived through `_arrive` proportionally), and leave every other wallet's round-kr credit where it is.

**Reproduction**

Fresh pool (INITIAL_HP = 1 IMD). Tx 1: bob buys 0.3 IMD exact-in via PoolSwapTest -> damage[1][bob] = 0.3e18. One transaction from a bundler EOA (tx.origin = bundler) that calls a router twice: (a) buyAndTake(2 ether, alice): exact-in buy that kills round 1, all BOSS taken to alice; (b) sellViaCustody(bob, half of bob's BOSS): transferFrom(bob -> router) then an exact-in sell settled from the router. Expected (identical to running (a) and (b) as two transactions, which the test checks first): damage[1][bob] = 0.3e18, damage[1][router] = 0. Actual: damage[1][bob] = 0.15e18, damage[1][router] = 0.15e18 and claimableDamage(1, router) > 0. Variant: replace (b) by transferBoss(bob -> dave, half): expected dave 0, actual bob 0.15e18 / dave 0.15e18. Run: forge test --match-path test/scratch/ClosedRoundBystander.t.sol (both tests fail with 150000000000000000 != 300000000000000000).

### 2. Low: Kill prize and killing credit move with any BOSS the killer sends in the kill transaction, even older BOSS, while the killer still holds the whole blow

`src/BossHook.sol:774`

```
            if (amount > hf) amount = hf;
```

`_trackKill` (and `_releaseClosed`, which runs before it on the same hop) cannot tell blow BOSS from older BOSS the same wallet already held, so a transfer out of a blow holder is counted blow-first: the whole transfer amount, capped at the holder's held-blow count, is deducted from `from` and added to `to`, `_recount` then hands the prize to the largest holder, and `_releaseClosed` moves the same proportional slice of the closed round's damage credit. A killer who batches the kill with an ordinary BOSS payment of older holdings in the same transaction (a smart-contract or ERC-4337 account executing kill + transfer in one call, or a router script that forwards some older BOSS after the swap) hands the entire killer prize and the entire killing-blow damage credit to the payee while still holding every wei of the blow. The README states the opposite for this case: 'other BOSS of that round moving in the same transaction carries its credit but never the prize'. Only the killer's own action triggers it (no third party can), but the loss is total and the trigger is an ordinary batched payment. Fix: count only the part of the transfer that exceeds the sender's non-blow balance as blow BOSS: after the transfer, blowOut = held > balanceOf(from) ? held - balanceOf(from) : 0 (one STATICCALL to the trusted BossToken, or have the token pass the sender's remaining balance), apply the same non-blow-first rule in `_releaseClosed` for the signer's closed-round credit, and keep the README sentence as the rule.

**Reproduction**

Fresh pool (INITIAL_HP = 1 IMD). Killer V holds 1,000,000 BOSS received from the deployer (no credit). ONE transaction with tx.origin = V through V's own router that custodies nothing: (1) exact-input buy of 2 IMD kills round 1; (2) poolManager.take(BOSS, V, all) delivers the blow (about 765,000 BOSS) to V: results(1).killer == V, unclaimedKill[V] == killerPrize (13,500,000,000,000,000 wei), damage[1][V] == 1e18; (3) boss.transferFrom(V, M, 1_000_000 ether) by the router: only older BOSS leaves, V keeps every wei of the blow. Expected: results(1).killer stays V, unclaimedKill[V] == killerPrize, unclaimedKill[M] == 0 (and damage[1][V] stays 1e18). Actual: held[V] is set to 0 and held[M] to the whole blow, _recount moves the prize: results(1).killer == M, unclaimedKill[V] == 0, unclaimedKill[M] == killerPrize; _releaseClosed moves all of the credit: damage[1][V] == 0, damage[1][M] == 1e18. Run: forge test --match-path test/scratch/BlowFirstPrize.t.sol (fails: killer is M, not V).

### 3. Low: A buy and a sell in one unlock, settled gross rather than net, charge the sold BOSS twice (pending take-back plus forfeit)

`src/BossHook.sol:515`

```
            _unpend(r, bossAmount);
```

In `_play` a sell always takes back this transaction's pending credit for the BOSS it hands in (`_unpend`), even when that BOSS really comes from the wallet. For the netted router shape (no token moves for the sell, one take of the difference) that take-back is the only charge and it is correct. A router can also settle the same unlock gross: it pays the sell's BOSS into the PoolManager with sync/transfer/settle from the wallet and takes all of the buy's BOSS (the Universal Router's SETTLE with an explicit amount followed by TAKE does this). That real transfer into the pool also reaches `onBossTransfer` -> `_release`, which forfeits the wallet's existing round credit for the same BOSS. The second charge is not offset: `_unpend` already removed that BOSS's share of the pending credit, and the take then credits the remaining pending to only `pb - s` bought BOSS while the wallet receives all of the buy's BOSS. Two economically identical trades (same balances, same HP, same fees) end with different credit, depending only on how the router settles. Honest players lose credit (s/b0 of their earlier damage is forfeited a second time); nobody gains directly, since the forfeited damage leaves eligibleDamage, so this is a correctness and fairness defect, not a theft. Not among the README's known limits. Fix: when BOSS from a wallet enters the pool in the same transaction and round as a sell that already took back pending credit, net that BOSS against the BOSS `_unpend` handed back before forfeiting (for example keep a transient counter raised in `_unpend` and consumed, not forfeited, first in the sink branch of `onBossTransfer`, crediting the matching bought on arrival), or defer the sell's take-back until settlement shows whether the BOSS came from the pending buy or from a wallet.

**Reproduction**

Pool BOSS/IMD, INITIAL_HP 1 IMD, no ability. Tx 1: wallet W (its own router) buys 0.2 IMD exact-in and takes the BOSS: damage[1][W] = 0.2e18, bought b0. Tx 2, one unlock, two shapes of the same trade: buy 0.2 IMD exact-in, then sell s = b0/2 BOSS exact-in. (a) Net: take the BOSS owed (B - s) to W. (b) Gross: sync/transferFrom s BOSS from W to the PoolManager and settle, then take all B BOSS to W. Both shapes leave W the same BOSS balance and the boss the same HP (the test asserts both). Expected: equal credit. Actual: (a) damage[1][W] = 299982983419970910, (b) damage[1][W] = 199982983419970911; the gross shape loses 0.1 IMD = d0 * s / b0 of credit. Run: forge test --match-path test/scratch/GrossSettle.t.sol (fails with 199982983419970911 != 299982983419970910).

### 4. Low: AbilityBoard.propose accepts an EIP-7702 delegated EOA as an ability, whose code can be re-pointed after approval without a new proposal, oracle review or delay

`src/AbilityBoard.sol:33`

```
        if (ability_.code.length == 0) revert NoCode();
```

The only on-chain check on a proposed ability is that the address has code. Since Pectra an EOA that signed an EIP-7702 authorization carries 23 bytes of code (0xef0100 || target), so it passes, and EXTCODESIZE reports 23 for it. Such an address is not a contract with a fixed, verifiable body: its owner can re-delegate it to any other code at any time with a new authorization, and the hook keeps STATICCALLing the same `activeAbility` address. The 12-hour public delay, the 24-hour interval and the oracle review (conditions a to e are checked against the source at that address at review time) are then bypassed for every later change of behaviour, contradicting the trust model ('The keeper cannot activate an ability without the oracle'). The effect stays bounded by the hook's clamp [0.25x, 4x] and the STATICCALL (no money moves), which is why this is low. Preconditions: the keeper proposes such an address and the panel approves it once (an explorer shows the delegation target's verified source, which can satisfy a reviewer). Fix: in `propose` (or in `acceptAbility`) reject addresses whose code starts with 0xef0100 (equivalently require code.length > 23, or pin extcodehash in the proposal and check it at accept and at activation), so an ability is always an immutable contract.

**Reproduction**

vm.etch(eoa, abi.encodePacked(hex"ef0100", target)) so eoa.code.length == 23, the exact code shape an EOA has after a 7702 authorization; vm.prank(keeper); board.propose(eoa, "swarm job 123"). Expected: revert (not an immutable contract). Actual: succeeds and pending() returns (1, eoa); then, with a valid oracle attestation for answer id*2+1 (issuedAt > proposedAt), hook.acceptAbility(a, sig) queues the delegated EOA (queuedAbility == eoa) instead of refusing it. On mainnet the EOA's owner can later sign a new authorization pointing to different code and the hook's next swap runs it under the same activeAbility address. Run: forge test --match-path test/scratch/Delegated7702.t.sol (fails: 'a delegated EOA must not be queued as the ability'; it passes once either propose or acceptAbility refuses the address).

**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 {HookFlags} from "src/HookFlags.sol";
import {BossToken} from "src/BossToken.sol";
import {BossHook} from "src/BossHook.sol";
import {OracleGate} from "src/OracleGate.sol";
import {AbilityBoard} from "src/AbilityBoard.sol";
import {IAbilityBoard} from "src/interfaces/IAbility.sol";

/// @dev Stands in for IMD; only `decimals()` matters here (no pool is initialized).
contract MockIMD {
    uint8 public constant decimals = 18;
}

/// @dev An ability body with verified-looking source: the delegation target of the EOA.
contract SomeAbility {
    function modifiers(bytes calldata) external pure returns (uint256, uint256) {
        return (10_000, 10_000);
    }

    function name() external pure returns (string memory) {
        return "some ability";
    }
}

/// @title AbilityBoard.propose accepts an EIP-7702 delegated EOA as an ability
/// @notice The only on-chain check on a proposed ability is `code.length != 0`. Since Pectra an
/// EOA that signed a 7702 authorization carries 23 bytes of code (0xef0100 || target), so it
/// passes, yet its owner can re-point that delegation to any other code at any time with a new
/// authorization, without a new proposal, oracle review or 12-hour delay. The hook keeps calling
/// the same `activeAbility` address, so the ability in force can change at will after one approval.
/// The test passes once either the board's `propose` or the hook's `acceptAbility` refuses it.
contract Delegated7702Test is Test {
    uint160 internal constant FLAGS = HookFlags.BEFORE_INITIALIZE | HookFlags.BEFORE_SWAP | HookFlags.AFTER_SWAP
        | HookFlags.BEFORE_SWAP_RETURN_DELTA | HookFlags.AFTER_SWAP_RETURN_DELTA;
    bytes32 internal constant QUESTION = keccak256("boss-ability-question-v1");
    uint256 internal constant ORACLE_KEY = 0x0DAC1E;

    PoolManager internal manager;
    MockIMD internal imd;
    BossToken internal boss;
    AbilityBoard internal board;
    BossHook internal hook;
    address internal keeper = makeAddr("keeper");
    address internal oracleSigner;

    function setUp() public {
        vm.warp(1_800_000_000);
        oracleSigner = vm.addr(ORACLE_KEY);
        manager = new PoolManager(address(this));
        imd = new MockIMD();
        boss = new BossToken();
        board = new AbilityBoard(keeper);
        bytes memory args = abi.encode(
            IPoolManager(address(manager)),
            address(boss),
            keeper,
            IAbilityBoard(address(board)),
            oracleSigner,
            QUESTION,
            uint256(1 ether),
            uint24(129_000),
            address(imd)
        );
        bytes memory initCode = abi.encodePacked(type(BossHook).creationCode, args);
        (bytes32 salt, address predicted) = HookFlags.mineSalt(address(this), initCode, FLAGS, 300_000);
        address addr;
        assembly ("memory-safe") {
            addr := create2(0, add(initCode, 32), mload(initCode), salt)
        }
        require(addr == predicted && addr != address(0), "hook deploy failed");
        hook = BossHook(addr);
    }

    /// @dev Expected: a delegated EOA is not an immutable contract and is refused, by the board or
    /// by the hook. Actual: the board accepts it and the hook queues it on a valid attestation.
    function test_delegatedEoaIsNotAcceptedAsAnAbility() public {
        address target = address(new SomeAbility());
        address eoa = makeAddr("delegated-eoa");
        vm.etch(eoa, abi.encodePacked(hex"ef0100", target)); // the exact code an EOA has after a 7702 authorization
        assertEq(eoa.code.length, 23, "delegation designator");

        vm.prank(keeper);
        (bool proposed,) = address(board).call(abi.encodeCall(AbilityBoard.propose, (eoa, "swarm job 123")));
        if (!proposed) return; // refused at the board: fixed

        (uint256 id, address pendingAbility) = board.pending();
        assertEq(pendingAbility, eoa, "the board holds the delegated EOA as pending");
        vm.warp(block.timestamp + 1);
        OracleGate.Attestation memory a = _attestation(id * 2 + 1);
        bytes memory sig = _sign(a);
        (bool accepted,) = address(hook).call(abi.encodeCall(BossHook.acceptAbility, (a, sig)));
        assertFalse(accepted, "a delegated EOA must not be queued as the ability");
        assertEq(hook.queuedAbility(), address(0), "nothing queued");
    }

    function _attestation(uint256 answer) internal view returns (OracleGate.Attestation memory a) {
        a.requestId = keccak256(abi.encode("req", answer, block.timestamp));
        a.chainId = block.chainid;
        a.questionHash = QUESTION;
        a.answerType = 3;
        a.answer = abi.encode(answer);
        a.panelJobId = keccak256("panel");
        a.panelSize = 21;
        a.quorum = 15;
        a.agreed = 18;
        a.issuedAt = uint64(block.timestamp);
        a.expiresAt = uint64(block.timestamp + 1 hours);
    }

    function _sign(OracleGate.Attestation memory a) internal view returns (bytes memory) {
        (uint8 v, bytes32 r, bytes32 s) = vm.sign(ORACLE_KEY, hook.attestationDigest(a));
        return abi.encodePacked(r, s, v);
    }
}
```

### 5. Info: launch.json pins the start price for one currency ordering only; if the factory's BOSS address sorts above IMD, initialize reverts WrongStartingPrice and the launch fails

`launch.json:34`

```
    "initialPrice": "125262255113908064987203232"
```

The hook derives the expected launch tick from the ordering it sees (-129000 when BOSS is currency0, +129000 when it is currency1) and accepts a start within 300 ticks of it. The machine-readable manifest field pool.initialPrice carries the single value 125262255113908064987203232 = getSqrtPriceAtTick(-129000), which is right only when the deployed BOSS address is numerically below IMD (0xd34a99bc0f67ae1bbd63c660e6d0b0dd03e263b7). The token address is assigned by the factory at deploy time ($token), so the ordering is not known when the manifest is written; the manifest's free-text notes state the mirrored value (getSqrtPriceAtTick(129000) = 50111677533496076234078224273595) but the field cannot express both and nothing in the tree selects between them. This is a safe failure (the hook refuses, nothing launches unscored), not a loss, but it is a concrete way the launch transaction reverts about half of the time. Fix: have the launch compute the start price from the ordering (hook.launchSqrtPriceX96(bossIsCurrency0) exists for this), or mine the token address so BOSS sorts below IMD, or make the launch tooling swap the value when BOSS sorts above IMD.

**Reproduction**

Deploy BossToken at an address above the paired currency (CREATE2 salt loop), deploy the hook with launchTickAbs 129000 and pairedCurrency = IMD, then call manager.initialize(key(IMD, BOSS, 12500, 60, hook), 125262255113908064987203232): reverts (WrongStartingPrice(sqrtPrice, -129000, 129000), wrapped by the PoolManager). The same key with getSqrtPriceAtTick(129000) = 50111677533496076234078224273595 initializes, and with BOSS below IMD the manifest value initializes. Verified by test/scratch/LaunchPrice.t.sol (test_bossAboveImd_manifestPriceReverts, test_bossBelowImd_manifestPriceInitializes, test_manifestPriceIsTickMinus129000 all pass).

---

Judge's submission `def755ccbd1c5ff30893d22f6d03a549a2cd331b3d9af38117d72a66d332608b`, 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.
