# 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; 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 and 853ff4df; 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 | `7dc01c203710db75af619953486500ce1268a3cc` |
| Job | `94b6c9a5-74a6-4703-b55f-c9440f6ee12d` |
| Judged | 2026-10-13 07:29 UTC |
| Findings | 1 medium · 1 low · 2 info |

Four agents audited the code as it is at `7dc01c2`, 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-paying custodian (Universal Router PAY_PORTION shape) hands the whole killing blow and the killer prize to the fee collector

`src/BossHook.sol:737`

```
        if (from.code.length != 0) {
            out = value < hf ? value : hf;
```

`_hopBlow` treats every transfer out of a contract as blow BOSS first (`out = min(value, held)`). The Uniswap app's interface-fee flow custodies the bought BOSS on the Universal Router, pays PAY_PORTION as a share of the router's BOSS *balance* to the fee collector, then SWEEPs the remaining balance to the player. Anyone can send BOSS to the router in an earlier transaction (the router has no code to refuse it), inflating that balance so the 0.25% portion exceeds the blow. The fee hop then carries every wei of the blow's credit (damage[kr]) and, through `_trackKill`/`_moveKill`, the killer prize to the fee collector; the sweep to the player finds held[router] == 0 and carries nothing. This contradicts the README's claims that 'an interface fee of a fraction of a percent ... never end[s] up with it' and that 'BOSS anyone planted on a deployed contract never holds the blow back': the plant does not hold the blow back, it pushes it to the fee wallet. The planter gains nothing (the player even receives the planted BOSS via the sweep), so it is griefing, but it costs only about 400x the last hit's BOSS (tiny when bots race for the last hit with small buys) while the victim loses the full killer prize (45% of the pot) plus the blow's damage share, and a player using the standard app flow cannot prevent it. Fix (design decision): the hook cannot tell planted from blow BOSS on a contract, so use a custodian rule robust to both shapes, e.g. attribute blow BOSS leaving a contract in proportion to the transfer's share of the contract's pre-transfer balance (out = held * value / (balanceAfter + value)) instead of blow-first; that keeps the prize with the sweep's recipient (99.75%) and limits the exact-forwarder plant of review 5 to diluting rather than stealing. Alternatively document that app users paying an interface fee can lose the kill to a planter. Merged from audit_permissions; reproduced on this commit with test/scratch/PlantedFeeCustodian.t.sol.

**Reproduction**

State: boss 1 at 1 IMD HP; tx 1: alice buys 0.99 IMD exact-in, 0.01 IMD HP left. tx 2: eve transfers 10,000,000 BOSS to the custodian router (a front-run). tx 3: carol (tx.origin) kills through the router with a 0.02 IMD exact-input buy; the router takes all BOSS to itself, pays 0.25% of its balance (~25,000 BOSS, more than the ~7,800 BOSS blow) to feeCollector, then sweeps the rest to carol. Expected (README): results(1).killer == carol, unclaimedKill(carol) == the prize, damage(1, carol) ~ the blow. Actual: results(1).killer == feeCollector, unclaimedKill(feeCollector) == 6817500000000000 (the whole prize), damage(1, feeCollector) == 10000000000000000 (the entire blow), damage(1, carol) == 0, unclaimedKill(carol) == 0. The control run without the plant leaves prize and credit with carol. Run: forge test --match-path test/scratch/PlantedFeeCustodian.t.sol -vv

### 2. Low: acceptAbility admits a mutable forwarder as an ability: its multipliers can change after approval with no new attestation, delay or interval

`src/BossHook.sol:467`

```
        if (len == 0 || (len == 23 && bytes3(ability.code) == 0xef0100)) revert AbilityNotAContract();
```

The only on-chain guard on what the oracle approved is that the proposed address holds code that is not an EIP-7702 designator ('what passes here keeps its code'). Keeping its code is not keeping its behaviour: a contract that forwards `modifiers(SwapInfo)` to an address held in storage (a proxy, a beacon clone, anything with a settable target) passes this check, is queued once the panel signs id*2+1, activates after the 12-hour delay, and from then on returns whatever its admin points it at. `_abilityMults` calls it on every trade with no re-check, so the forwarder's admin (the keeper, or whoever controls it) moves the live multipliers anywhere in [0.25x, 4x] at will, bypassing the oracle, the 12-hour public wait and the 24-hour minimum interval the README's trust model relies on ('The keeper cannot activate an ability without the oracle'; 'every change waits 12 hours in public'). No ability can move money and the clamp still applies, so the damage is limited to game fairness (a heal boost switched on seconds before a large sell, a hit nerf when the boss is nearly dead), which keeps this low. The intended defence is condition (d) of the oracle question ('no external calls'), enforced by the panel only: one approval of a thin forwarder (verified source, no state writes, returns two numbers) hands its admin permanent control, and the keeper and signer are immutable so a panel error here is unrecoverable. Fix options: (1) at accept time refuse any ability whose runtime bytecode contains CALL/STATICCALL/DELEGATECALL/CALLCODE opcodes outside PUSH data (condition (d) forbids external calls anyway, so this rejects nothing the panel would approve); (2) have the attestation cover the behaviour the panel read (e.g. an answer encoding a hash the hook re-derives from a probe set of `modifiers` calls); or at minimum (3) state in the README that line 467 pins the address, not the behaviour. Merged from audit_flow; reproduced on this commit with test/scratch/MutableAbility.t.sol.

**Reproduction**

State: hook launched, pool initialized, no ability active. Keeper proposes MutableProxy(impl = GoodAbility returning (10000, 10000)); the oracle signs answer id*2+1 for that id; acceptAbility succeeds; 12 hours later a 0.01 IMD exact-input buy removes 0.01 IMD of HP (1x). The proxy admin calls setImpl(HarshAbility returning (2500, 40000)). Expected: the ability in force keeps the behaviour the panel approved until a new attestation is accepted, waits 12 hours and respects the 24-hour interval. Actual: the very next 0.01 IMD buy removes 2500000000000000 (0.0025 IMD, 0.25x) of HP while activeAbility is still the proxy and acceptedId has not moved; no attestation, delay or interval was involved. Run: forge test --match-path test/scratch/MutableAbility.t.sol -vv (fails with 2500000000000000 != 10000000000000000).

### 3. Info: Attestation issued in the same second as the proposal is refused as stale (off-by-one on the freshness boundary)

`src/BossHook.sol:469`

```
        if (a.issuedAt <= board.proposedAt()) revert StaleAttestation();
```

acceptAbility treats an attestation whose issuedAt equals AbilityBoard.proposedAt as stale. Both are whole seconds, so an oracle that observes the proposal in the block that includes it and signs with issuedAt equal to that block's timestamp produces an attestation the hook can never accept, although it was issued after the proposal was recorded and refers to the right (id, ability) pair. The strict comparison buys no security: the answer is already bound to the board id, which increments on every propose, so an attestation for id N cannot approve any other proposal, and acceptedId stops replay. The only effect is a liveness nit: the oracle has to issue a second attestation one second later. Fix: use `<`, or document that the oracle must issue strictly after the proposal's block timestamp. Merged from audit_math; reproduced on this commit with test/scratch/SameSecondAttestation.t.sol.

**Reproduction**

State: keeper calls board.propose(ability, note) at block.timestamp T (proposedAt = T, id = 1). Input: a valid attestation for answer 3 (= 1*2+1), panelSize 21, quorum 15, agreed 18, issuedAt = T, expiresAt = T + 1h, signed by the oracle signer; call hook.acceptAbility(a, sig) in a block with timestamp T. Expected: the ability is queued (AbilityQueued). Actual: revert StaleAttestation(). With issuedAt = T + 1 and block.timestamp = T + 1 the same call succeeds (control test in the same file). Run: forge test --match-path test/scratch/SameSecondAttestation.t.sol -vv

### 4. Info: Forfeit slice rounds down in favour of the wallet handing BOSS back (sub-wei credit retention, not economically exploitable)

`src/BossHook.sol:904`

```
        return (dmgTotal * value / bossTotal, value);
```

`_slice` computes the credit that leaves with `value` BOSS as `dmgTotal * value / bossTotal`, rounded down. On the forfeit path (`_release` called from `onBossTransfer` when `to` is the PoolManager or the hook) this rounding favours the wallet handing BOSS back: any transfer of fewer than bought/damage wei of BOSS lowers `bought[r][from]` but forfeits zero damage. At the launch price (tick 129000, about 2.6e-6 IMD per BOSS) that window is about 3.8e5 wei of BOSS per transfer (3.8e-13 BOSS), so draining a real position's forfeit this way takes on the order of 1e17 transfers and is not a practical attack; the pot, the eligible-damage denominator and other wallets' shares are only affected by sub-wei dust. Reported because a forfeit is a charge against the leaving wallet and the convention is to round such charges up so the protocol never loses the dust. Minimal fix: round the forfeit up on the sink path only (e.g. (dmgTotal * value + bossTotal - 1) / bossTotal, capped at dmgTotal), keeping the wallet-to-wallet move and the pending-delivery paths rounded down. Merged from audit_economics; reproduced on this commit with test/scratch/ForfeitRounding.t.sol.

**Reproduction**

Fixture: BOSS/mock-IMD pool at tick |129000|, initial HP 1 IMD. 1) alice buys with 0.1 IMD exact-in: damage[1][alice] = 100000000000000000, bought[1][alice] = 38285676337265786184662. 2) alice transfers 382856 wei of BOSS (bought/damage) straight to the PoolManager (a sink). Expected under 'forfeits in proportion': a forfeit of at least 1 wei of damage. Actual: damage[1][alice] stays 100000000000000000 while bought[1][alice] drops to 38285676337265785801806 (the slice 1e17*382856/3.8285676e22 rounds to 0). Run: forge test --match-path test/scratch/ForfeitRounding.t.sol -vv

---

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