# 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 open-round credit; once a round closes its credit is frozen except the killing blow's, which travels with its BOSS and keeps the prize with its largest holder (the router that executed the kill pays it out in proportion to its balance); 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 must be contracts written by the swarm, accepted only with an IMD oracle attestation, 21 seats, quorum 15). This commit fixes the findings of jobs be67d693, 853ff4df and 94b6c9a5; 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 | `5acf4b463850d4756e223dc7321570b3e4c526a3` |
| Job | `965a5fce-0269-4f65-8079-bf6793d5774c` |
| Judged | 2026-10-13 08:48 UTC |
| Findings | 1 medium · 2 low · 2 info |

Four agents audited the code as it is at `5acf4b4`, 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: Planted BOSS on a fee-then-exact-delivery killing router hands the killer prize to the interface fee wallet and strands ~99% of the blow's credit on the router

`src/BossHook.sol:748`

```
            out = hf * value / (IBossToken(bossToken).balanceOf(from) + value);
```

`_hopBlow` pays the killing blow out of the killing locker pro rata to the locker's whole BOSS balance, and `_weight` (line 866) drops the locker to weight 0 (T_KILL_PAID) as soon as it pays any wallet. Both rules assume the player ends up with the largest share of the blow. A custodial router that takes an interface fee as a share of its balance (the Universal Router's PAY_PORTION shape) and then transfers exactly the swap output minus the fee to the player (no SWEEP, the shape most aggregators use) breaks that assumption as soon as anyone has parked BOSS on it in an earlier transaction: the fee transfer carries `bips` of the blow, but the exact delivery carries only output/(plant+output) of the rest. With a plant of a few hundred times the output the player's share drops below the fee wallet's, `_trackKill` -> `_recount` sees the locker at weight 0 and the fee wallet as the largest holder, and `_moveKill` sends the whole 45% killer prize to the interface fee wallet. The rest of the blow's damage credit (~98.7% in the reproduction) stays on the router; if the router has a public sweep (the Universal Router does) anyone can `claimDamage(kr, router)` and sweep it. The README's Known limits state that an exact-amount delivery keeps the prize with the player and that planted BOSS 'never' keeps the prize away from the player; the fee-then-exact shape is not covered and gives the prize to a third party. Cost to the griefer: ~2.3M BOSS (about 6 IMD at the launch price), recoverable only if the router is sweepable; the player loses the prize, the fee wallet gains it. Suggested minimal fix that keeps the design: in `_trackKill`/`_recount` only move the prize away from the credited killer to a wallet whose held blow BOSS exceeds half of T_KILL_BOSS (a strict majority), otherwise leave it with `results[kr].killer`; alternatively weigh the locker's payouts by raw BOSS only when its balance before the killing swap was zero.

**Reproduction**

test/scratch/Judge.t.sol::test_feeThenExact_plant_movesPrizeToFeeWallet (BossFixture, INITIAL_HP = 1 IMD, mock IMD). (1) alice buyExactIn(0.99 ether): hp = 0.01 IMD. (2) A FeeExactRouter R (R7Router + buyFeeThenExact: buys to itself, transfers balance*bips/10000 to feeTo, then transfers exactly output*(1-bips/10000) to the user) funded with 1 IMD. (3) eve transfers 300 * 7650.95 BOSS = 2,295,285 BOSS to R in an earlier transaction. (4) carol (tx.origin) calls R.buyFeeThenExact(0.02 ether, carol, feeCollector, 100). Expected (README Known limits): results(1).killer == carol, carol's round-1 damage ~0.0099 IMD, feeCollector 1%. Actual: results(1).killer == feeCollector, unclaimedKill[feeCollector] == 6817500000000000 (the whole prize), unclaimedKill[carol] == 0; damage[1][carol] == 32890365356168 (0.33% of the blow), damage[1][feeCollector] == 99999999999999 (1%), damage[1][R] == 9867109634643833 (98.7%, stranded on the router). Control without the plant (test_feeThenExact_noPlant_control): killer == carol, damage[1][carol] == 9900000000000001, feeCollector 1%.

### 2. Low: bindHook authorizes by tx.origin only: any contract the deployer EOA calls between the token and hook deployments can bind itself as the hook and freeze every BOSS transfer

`src/BossToken.sol:54`

```
        if (tx.origin != _launchOrigin) revert NotLaunchOrigin();
```

`bindHook` has no check on msg.sender beyond `tx.origin == _launchOrigin`. If the token and the hook are not created in the same transaction, every call the deploying EOA makes to any contract in between (a claim, an approval through a dapp, a relayed launch step) lets that contract call `bindHook()` and become the permanent hook. The binding is one-shot, so the real BossHook constructor then reverts with HookAlreadyBound, and because `_transfer` calls `onBossTransfer` on whatever is bound, a hook that reverts freezes all BOSS transfers with no recovery (no owner, no rebind). The README relies on the swarm factory deploying token and hook atomically; the contract does not enforce it. Document as a launch trust assumption or harden: bind to a precomputed CREATE2 hook address in the token constructor, or require the binder to be the expected hook / a contract deployed by the token's deployer.

**Reproduction**

test/scratch/Judge.t.sol::test_bindHook_phish. deployer (EOA, vm.prank(deployer, deployer)) deploys BossToken in tx 1. In tx 2 the deployer calls Phish.claimAirdrop(), where Phish calls boss.bindHook() and Phish.onBossTransfer reverts('blocked'). Expected: only the BossHook can be bound. Actual: boss.hook() == address(Phish); boss.transfer(address(1), 1) from the deployer reverts with 'blocked'; a later BossHook constructor would revert with HookAlreadyBound. Control: the same call from a stranger (eve) reverts NotLaunchOrigin.

### 3. Low: A credit-capped buy still adds its BOSS to `bought`, padding the forfeit denominator so a later sell of a full buy keeps credit the README says it forfeits

`src/BossHook.sol:830`

```
        _credit(uint64(_tload(T_PEND_ROUND)), to, dmg2, boss2);
```

`_cappedPending()` cuts T_PEND_DAMAGE to the HP this transaction removed, but T_PEND_BOSS is left untouched, so `_arrive` calls `_credit(r, to, 0, boss2)` and `bought[r][to]` grows by BOSS that carries no damage. `_release` forfeits proportionally to `bought`, so this damage-free BOSS pads the denominator: after a later real buy of X BOSS with damage D, handing back exactly those X BOSS forfeits only D*X/(padded+X) instead of D. The README rule 'selling all of it loses all of it' (proportional forfeit) is not what the code enforces once a wallet has taken a capped buy. Precondition: a heal earlier in the same transaction that exceeds the buy's damage, i.e. the same precondition as the documented two-wallet limit (audit be67d693 finding 7); the economic effect is bounded by what that limit already allows, so low. Fix: scale T_PEND_BOSS down with the damage when the cap bites (boss2 = pb * pd_capped / pd_raw) in `_arrive` and `_kill`, or do not add BOSS to `bought` when dmg2 == 0.

**Reproduction**

test/scratch/Judge.t.sol::test_cappedBuyPadsBought (BossFixture + test/utils/ScriptRouter.sol funded with 100,000 IMD). (1) carol buyExactIn(0.6 ether): hp = 0.4 ether. (2) bob holds 400,000 BOSS from the deployer (credit-free). (3) One transaction signed by alice, two unlocks of the router (runMany): unlock A = pullFrom(bob, router, 400_000 ether), sellOwn(400_000 ether) (heals to 0.6777 ether, forfeits nothing); unlock B = buy(0.3 ether), takeBoss(alice, max). Observed: damage[1][alice] == 0 (cap bit), bought[1][alice] == 114847257889700052885796 (padded, damage-free). (4) alice buyExactIn(0.2 ether): damage 200000000000000000, bought grows by 76548556160233219136303. (5) alice sellExactIn of exactly 76548556160233219136303 BOSS. Expected: damage[1][alice] == 0. Actual: damage[1][alice] == 120010208645145751 (60% kept) while all BOSS of that buy was handed back and the boss healed by it.

### 4. Info: A trade whose scaled damage or heal truncates to zero is dropped before any game bookkeeping, so the README's 'a trade under 40 wei still hits or heals' does not hold under a sub-1x ability

`src/BossHook.sol:523`

```
            if (dmg == 0) return;
```

In `_play`, a buy computes dmg = BossMath.scale(gross, buyMult) and returns before `_pend`/`_kill` when it truncates to zero. With an ability at the clamp floor buyMult = 2500, every buy with gross <= 3 wei is a no-op: hp unchanged, no pending damage, no `bought` entry, so the BOSS that leaves the pool for it carries no credit. The heal side truncates the same way (heal = scale(gross, sellMult), line 532; a 1 wei BOSS sell pays ~0 wei and heals nothing). README.md:195 claims such a trade 'still hits or heals'. Bookkeeping stays self-consistent and the amounts are far below gas cost: documentation accuracy only, no exploit.

**Reproduction**

test/scratch/Judge.t.sol::test_minMult_3wei_noHit. Activate FixedAbility(2500, 2500) by writing hook.activeAbility via stdstore (as test/invariant/BossInvariant.t.sol does); buyExactIn(alice, 3). Expected per README: the boss is hit. Actual: hook.hp() stays 1000000000000000000 and no Hit event. Control buyExactIn(alice, 4): hp becomes 999999999999999999 (1 wei hit), pinning the cutoff at gross*buyMult < 10000.

### 5. Info: The killer prize goes to whoever lands the last hit regardless of contribution, which makes the kill a public-mempool race a searcher wins by front-running the finishing buy

`src/BossHook.sol:529`

```
            if (current == dmg) _kill(signer);
```

Agreed design (README: the wallet that lands the killing blow gets the killer share), reported so the launch documentation can state it. The killer prize is 45% of the pot and goes to whoever makes hp reach 0, however small that hit is; damage already done only earns a share of the other 45%. Remaining HP is public and every pending buy is visible in the mempool, so a searcher can front-run any buy that would kill with an exact-input buy of hpLeft plus dust, take the prize, and the victim's buy lands on the fresh boss at 1.5x HP. Nothing in the code limits this and the README's Known limits do not mention it. Least-change handling: document it and recommend a private relay for finishing buys; if the economics are to change, weight the killer prize by the killing transaction's share of the round's damage.

**Reproduction**

test/scratch/Judge.t.sol::test_tinyLastHit (BossFixture, INITIAL_HP = 1 ether). (1) alice buyExactIn(0.99 ether): hp left ~0.01 ether, alice holds ~99% of round-1 damage. (2) bob buyExactIn(hpLeft + 1e15) in the next transaction. Actual: results(1).killer == bob, unclaimedKill[bob] == killerPrize == 6756750000000000; claimableDamage(1, alice) == 6689182500000000, claimableDamage(1, bob) == 67567500000000. bob, with ~1% of the damage, collects 6824317500000000 against alice's 6689182500000000.

---

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