# Audit report

> Audit the money of the delta of moneycanbuy.fun between commit 84b446c and commit 14c4e30, four changed payout and lock rules. Repository at commit 14c4e308019a0cff83fdaf2d17425867af3cf301. Run forge at the repo root, import as "src/...". Modify no existing file; new files only under contracts/test/audit/.
>
> SOURCE OF TRUTH. The RULES below are the money rules for these four changes; docs/MECHANICS.md and docs/YOLO.md restate them. Code or docs off the RULES = a finding. Do not re-report what contracts/test/audit/ proves fixed.
>
> SCOPE. Only the money effect of these four changes between commit 84b446c and commit 14c4e30. Not a full payout audit: ignore anything unchanged in this range. Code security and role scenarios are separate orders: one info line each if relevant.
>
> THE FOUR CHANGES AND THEIR NEW RULES.
> 1. Lock extension power. YoloLockHub.lock(): extending (a call with a nonzero new term) now fixes power = amount * weeks_left / 52 (the whole current amount at the new term), replacing the old max(old_power + added*weeks_left/52, amount*weeks_left/52). Adding without a new term still does old_power + added*weeks_left/52.
> 2. Stake-bonus base = the whole pot. CaseDesk._stakeBps(): the doer stake bonus b = 4% x min(1, power / R) now uses R = DevPrices.yoloValue(t.currency, t.pot) where t.pot is the task's whole reward at submission (the author's own share plus every top-up), replacing R = yoloValue(t.currency, t.base) (the author's own share only).
> 3. $YOLO add-on in the stake-bonus base. After computing r from t.pot, CaseDesk._stakeBps() now does r += t.addon, so the YoloSweetener $YOLO add-on is added to R too; the bond base (t.base) is unchanged, author's own share only.
> 4. Predict pokes the task first. YoloPredict.predict(): if a submission is active and the deadline has not passed, predict() now calls vault.poke(taskId) and re-reads task status before the SubmissionActive check, so a submission whose own timer already ended no longer blocks new positions; no money rule changes other than when the window opens.
>
> Prices (no network): IMD $4.50, ETH $2,398, YOLO $0.001 (parameter), swarm call 0.5 IMD = $2.25, gas 0.12 gwei; in code every price is a dev-wallet parameter, USDC = $1.
>
> KNOWN LIMITATIONS. Every line under Known limitations in docs/MECHANICS.md is an accepted rule, not a finding, including the self-dispute-from-a-second-wallet line and its ~4.5% of R example. Report one only if a changed line above makes a documented rule itself economically worse than its own stated number.
>
> GOAL. Every high and medium finding with a Foundry PoC; low and info in a separate list at the end, one line each, no PoC needed.
>
> REPORT FORMAT, MANDATORY. One entry per item below, same order, even "no issue"; a missing item = INCOMPLETE. Entry 0: VERDICT, max 8 lines: payouts vs RULES, every money blocker. Items 1-6: what you checked, file:line, severity (critical = theft, permanent loss or freeze; high = a RULES amount broken; medium = bounded loss or profitable gaming; low = dust, edge case; info = drift), loss in tokens and $ at a stated R, smallest fix, a Foundry PoC (full source, contracts/test/audit/, exact amounts, fails now, passes once fixed, no fork, no ffi) if real, else one line why not.
>
> 1. LOCK POWER AMOUNTS. $100 task, YOLO at $0.001: a doer locks 50,000 YOLO for 52 weeks (power 50,000), waits 26 weeks (power unchanged per docs), then extends to a new 52-week term. Compute power before and after under both the old max()-formula and the new amount*weeks_left/52 formula; state the $ difference in the doer's stake bonus b this enables or removes, and whether the new rule can ever let a doer buy a higher power for the same YOLO than locking the full amount fresh for 52 weeks would cost.
> 2. STAKE BASE t.pot vs t.base, WORKED EXAMPLE. $100 task, author put in $60 of it (t.base=$60), others topped up $40 (t.pot=$100), no $YOLO add-on. A doer with lock power worth $100 in YOLO: under the old rule (base=t.base=$60) b would floor at 4% (power >= base); under the new rule (base=t.pot=$100) b is still 4% here, but power worth $60 now gives b=2.4% instead of 4% under the old rule. State exactly which doers gain and which lose stake-bonus $ from this change, at $100 and at $10,000 reward, and whether any party can engineer t.pot upward (extra top-ups) or downward right before submit() to move b in their favor at someone else's expense.
> 3. ADD-ON IN STAKE BASE, WORKED EXAMPLE. $100 task, $50 $YOLO add-on from YoloSweetener (t.addon = 50,000 YOLO at $0.001). Old rule: stake base = t.base only, add-on excluded from the power comparison but bonus still paid on the add-on amount. New rule: base = t.pot + t.addon = $150 worth. Give the doer's bonus $ under both rules for a doer with power worth $75, at $100 and $10,000 reward (scaled add-on), and state who pays for the difference (burn vs treasury split, per docs/MECHANICS.md "(5% - b) half burn, half treasury").
> 4. STAKE BASE INFLATION ATTACK. Can the author or a colluding contributor top up the task (raising t.pot) or deposit a sweetener add-on (raising t.addon) in the same transaction as, or immediately before, a submission, specifically to lower a hostile doer's b below what their real lock power should earn, or to raise their own colluding doer's apparent eligibility? Give the cheapest such plan and its $ cost vs $ gained at $100 and $10,000 reward, or state why t.pot/t.addon cannot be moved in time for a specific submission.
> 5. PREDICT WINDOW TIMING, NOT AMOUNTS. Confirm the poke-before-check change in YoloPredict.predict() moves no money by itself (no split, no claim changes): state in one line whether it only changes when a position is accepted, with no $ effect of its own, or whether poke's side effects (e.g. expiring a task) change any payout amount.
> 6. COVERAGE. What was checked for these four changes, forge build and test result (passed/failed).
>
> Not findings: anything unchanged between 84b446c and 14c4e30; test/mocks, brand/, landing/; the known-limitations self-dispute line itself.

| | |
|---|---|
| Repository | https://github.com/sergey-mavr-agent/moneycanbuy.fun.git |
| Commit | `14c4e308019a0cff83fdaf2d17425867af3cf301` |
| Job | `7a9950a7-b480-4315-8298-0d990571d9af` |
| Judged | 2026-10-20 10:58 UTC |
| Findings | 1 low · 2 info |

Four agents audited the code as it is at `14c4e30`, 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: lock(): a term that lands exactly on the current end is accepted as an extension and only re-fixes (lowers) the running power

`contracts/src/YoloLockHub.sol:73`

```
            if (newEnd < end) revert BadTerm();
```

Change 1 (line 84: `uint256 pw = weeks_ != 0 ? amount * left / MAX_WEEKS : old + add * left / MAX_WEEKS;`) makes every call with a nonzero term an extension that fixes power = amount x weeks left / 52, as the RULES say. The term guard at line 73 rejects only a strictly earlier end, so lock(add, k) with block.timestamp + k weeks == the current end passes as an 'extension' although the end does not move, and the natspec at lines 17-18 ('the end can only move later') and docs/YOLO.md ('the end only moves later') do not match what is enforced. The one effect of such a call is a power drop: the whole amount is re-rated at the weeks left (50,000 YOLO, 26 weeks served of 52: power 50,000 -> 25,000; adding 10 YOLO with the remaining 26-week term gives 25,005 instead of 50,005 from lock(10, 0)). Same mechanism, rule-compliant: a term that lengthens the lock by less than the full 52 weeks (e.g. 30 weeks with 26 left) also lowers the power (50,000 -> 28,846). Money effect is self-inflicted only: the doer's next stake bonus falls; a bonus already fixed at an earlier submission is kept (powerAt/stakePower read the earlier checkpoint), and no third party gains or loses. At YOLO $0.001 on a $100 task (R = 100,000 YOLO): b 2% -> 1%, $2.00 -> $1.00 per accepted task, the difference going half to the burn, half to the treasury; same $1.00 at $10,000. The new rule never yields more power than a fresh 52-week lock of the whole amount (power <= amount in both branches, fuzzed), so no doer can buy power cheaper than before; the old max() formula was >= the new one in every case. In the task's own scenario (extend to a new 52-week term after 26 weeks) power is 50,000 under both formulas: $0 difference. Merged from audit_math, audit_permissions (2), audit_flow, audit_economics, which all describe this one behaviour. Smallest fix: when weeks_ != 0 and newEnd == end, treat the call as add-only (pw = old + add * left / 52), or revert BadTerm when add == 0; or state the trap in the lock quote on the site.

**Reproduction**

Reproduced on 14c4e30 with contracts/test/scratch/JudgeDelta.t.sol (FOUNDRY_OPTIMIZER=false forge test --match-path ...): doer2 hub.lock(50_000e18, 52); vm.warp(+26 weeks - 1 s) so exactly 26 weeks are left; hub.power(doer2) == 50_000e18; doer2 hub.lock(0, 26): the call succeeds, locks(doer2).end is unchanged, hub.power(doer2) == 25_000e18. Expected (natspec 'the end can only move later', old max() formula): BadTerm or power unchanged at 50_000e18. Actual: power halved. Control: doer hub.lock(0, 52) in the same state keeps 50_000e18. Fuzz (256 runs): after any lock/extend/add sequence power <= total amount locked.

### 2. Info: YoloLockHub notice and the hub test header still describe the stake-bonus base without the $YOLO add-on

`contracts/src/YoloLockHub.sol:13`

```
///         the CaseDesk reads `powerAt` at a submission and compares it with the task's whole reward in $YOLO
```

Change 3 (CaseDesk._stakeBps line 527, `r += t.addon;`) measures the power against the pot in $YOLO plus the task's $YOLO add-on, as the RULES, docs/MECHANICS.md and docs/YOLO.md state. The hub's contract notice (lines 13-14: '4% x min(1, power / reward)') still names the whole reward only, and the test header of contracts/test/YoloLockHub.t.sol lines 12-13 says 'the whole pot at the submission (no add-on)'. Docs drift only: the paying code is on the RULES (test/MoneyRules.t.sol test_AddonInPowerBase asserts 200 bps for 1M power against $100 + a 1M add-on). Merged from audit_permissions and audit_economics (two entries). Fix: reword the two comments to 'the pot in $YOLO plus the $YOLO add-on'.

**Reproduction**

Read contracts/src/YoloLockHub.sol lines 13-14 and contracts/test/YoloLockHub.t.sol lines 12-13 against contracts/src/desks/CaseDesk.sol lines 523-528. Scratch test (JudgeDelta.t.sol test_Item3_AddonInBase_PowerWorth75, YOLO at $0.0001): $100 USDC task + 500,000 YOLO add-on, doer power 750,000 YOLO -> stakeBps 200 (750k / 1.5M), not 300 as the notice's 'power / reward' would give.

### 3. Info: Stake bps fixed at submission against pot + add-on are paid on the final pot and final add-on: a top-up during the review earns the bps fixed against the smaller base (unchanged mechanism)

`contracts/src/desks/CaseDesk.sol:468`

```
        if (o == Outcome.Accepted) s.reward = SafeCast.toUint128(_bank().potOf(taskId));
```

Changes 2 and 3 fix R = yoloValue(t.pot) + t.addon as of the submission block (line 523-527); the bonus is then paid as s.stakeBps of s.reward, re-read as the final pot at acceptance (line 468), and YoloSweetener pays the same bps on bonusOf at payout. So a pot or add-on grown after the submission earns the bps fixed against the smaller base. This is the cheapest sequencing plan for item 4 of the order: author creates a $5 task, a doer with $5 of power submits (400 bps), the author tops up $9,995 during the 24 h review, the doer takes 4% of $10,000 = $400 from the fee; the treasury gets $50 instead of $1,000 (no stake) and the burn $550. The pair still pays the $1,000 fee minus the $400, and an honest staker with $10,000 of power would be paid the same, so it is a bypass of the stake requirement, not a loss to a third party. The mechanism is identical at 84b446c (R was t.base, also fixed at submission and also paid on the final pot), so by the order's scope it is not a finding of this delta; recorded as info. Pre-submission moves cannot be used against a doer: a top-up or an add-on deposited right before a submit lowers the bps but not the bonus dollars (bonus = 4% x min(power, R + A) when nothing changes afterwards), and neither can be withdrawn while the task is open (TaskVault.withdrawContribution reverts TaskNotOpen; YoloSweetener.withdraw reverts BonusLocked while a submission is in flight), so the base cannot be inflated and then deflated during the review. Fix, if wanted (a design change, out of this delta): pay the bonus on min(final pot + add-on, pot + add-on at submission) or re-rate the bps at acceptance.

**Reproduction**

Reproduced on 14c4e30 with contracts/test/scratch/JudgeDelta.t.sol (YOLO at $0.0001 in Base: $5 = 50,000 YOLO). test_Item4_PostSubmitTopUp_PaidAtFixedBps: doer hub.lock(50_000e18, 52); author _create(usdc, 5e6); doer submits -> stakeBps == 400; author topUp(t, 9_995e6) under review; 24 h later poke -> Completed, getSettlement(s).stakeBps == 400, credits(usdc, doer) == 9_000e6 + 400e6 + 0.15e6, treasuryPot(usdc) == 50e6. Controls: test_Item4_PreSubmitTopUp_NeverLowersBonusDollars ($100 task, alice tops up $100 before the submit of a doer with $100 of power -> stakeBps 200 but credits 180e6 + 4e6 + 3e6, the same $4; alice's withdrawContribution reverts TaskNotOpen) and test_Item4_PreSubmitAddon_CannotBeWithdrawnDuringReview ($100 add-on before submit -> 200 bps; sweetener.withdraw reverts BonusLocked; doer gets $2 on R + 20,000 YOLO = $2 on the add-on = the same $4).

---

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