Docs/Engineering/Audit History
shipped

Audit History

Record of external and internal security review of the ARU contracts. The active v2 design was audited by Nethermind Security (NM-0902) with zero findings; v1 review findings are retained below for the audit trail.

Audit History

This document consolidates every security review conducted against the ARU contracts. The current production design (v2, per-request) completed an external audit with zero findings; the earlier v1 (epoch-batched) review findings are preserved below for the audit trail.

External audit — v2 (Nethermind Security, NM-0902)

The active production contracts were independently audited by Nethermind Security. The review reported no issues at any severity.

Read the full report → NM-0902 — ARU Reserve (PDF)

Auditor Nethermind Security
Report NM-0902 — ARU Reserve, Security Review
Date June 23, 2026
Commit 1153227 (initial = final — no changes required)
Scope ARU.sol, ARUPriceOracle.sol, ARUSubscriptionManager.sol (578 LoC)
Findings 0 — Critical 0 / High 0 / Medium 0 / Low 0 / Undetermined 0 / Informational 0 / Best Practices 0
Test suite assessment High (113 tests, all passing)
Documentation assessment Low (in-repo docstrings; the extended docs you're reading now were not part of the review package)

The audited commit 1153227 is the current master HEAD of aru-evm, so the audited source and the deployed source are identical. On-chain bytecode can be reproduced from that commit — see Contracts & Addresses for the forge inspect procedure.

Scope note: the v2 audit covered only the per-request contracts. The v1 ARUMintRedeemManager (epoch-batched) has since been removed from the codebase and was not part of NM-0902. Its earlier review history follows.

v1 review history (superseded design)

The findings below were identified during internal and external review of the first-generation ARU contracts (ARUMintRedeemManager, epoch-batched) and were all resolved before that design was retired. The active production design is v2 — see Per-Request Flows — which does not carry forward the epoch state machine.

Documented here to preserve the audit trail and inform future reviews.

Internal review (Findings #1–#7)

#1 — Partial settlement stranded funds (High)

Issue: In the original completeMints / completeRedemptions, the epoch settled-flag was flipped before the per-AP loop. Any AP omitted from the aps array had non-zero request state that could never be processed. removeAP also rejected settled epochs, closing the only refund path.

Fix: Idempotent batched drain. completeMints removed the pre-loop flag flip; state is drained per-AP across however many batch calls are needed. Price is locked on first drain (epochMintPrice mapping) to prevent admin from approving later batches at a different price. Added closeEpochMints(epoch) which only succeeds once epochTotalMintCollateral[epoch] == 0, flipping the final epochMintSettled flag.

Status: Resolved in 8470866. Regression test: test_removeAP_afterPartialDrain_omittedAPRefundable.

#2 — epochTotal* accounting drift (Medium)

Issue: Settlement loops never decremented epochTotalMintCollateral or epochTotalRedeemARU on the redeem side. Only refunds decremented. Post-settlement totals were permanently stale.

Fix: Added epochTotalMintCollateral[epoch] -= collateralAmount inside the settlement drain loops. Restores the invariant sum(requests) == epochTotal* at all times.

Status: Resolved in 8470866. Critical for #1's close precondition (epochTotal* == 0) to work.

#3 — O(1) epoch advancement (Low–Medium)

Issue: The updateEpoch modifier used a while loop iterating once per missed epoch. After long inactivity (~4000+ epochs), catch-up gas exceeded block limit — full DoS.

Fix: Replaced with elapsed / EPOCH_DURATION integer division. Single EpochAdvanced event per advance regardless of skip count. Constant gas.

Status: Resolved in 8470866. No heartbeat keeper needed.

#4 — Oracle strict variant (Low)

Issue: ARUPriceOracle.stalenessThreshold was stored but never enforced on-chain. Integrators expected Chainlink-compatible strict semantics from the parameter name, but the oracle silently returned stale prices.

Fix: Added latestRoundDataStrict() that reverts PriceStale when block.timestamp > priceTimestamp + stalenessThreshold. Existing latestRoundData() untouched for Chainlink compatibility.

Status: Resolved in 8470866. Makes stalenessThreshold actually load-bearing.

#5 — removeAP skip-empty (Low)

Issue: Inner loop in removeAP reverted NoRequestInEpoch when an epoch had zero amount for the AP. Admin tooling needed bit-perfect off-chain reconciliation to avoid reverts — operational friction with no security benefit.

Fix: Changed revert NoRequestInEpoch to continue. Admin can pass superset lists.

Status: Resolved in 8470866.

#6 — Threat model documentation

Not a code finding — established explicit threat model covering 8 attack scenarios (T-1 through T-8), roles + powers table, accepted risks, and off-chain invariants the contracts assume. See Threat Model.

#7 — Invariant test suite

Not a code finding — established Foundry invariant-test harness design for future expansion. See Invariant Tests.

External review (Findings L-01, I-01, I-02)

L-01 — removeAP stranded funds after incomplete first call (Low)

Issue: Distinct from Finding #5 — after the first removeAP call deactivated the AP (isAP[ap] = false), subsequent calls reverted with NotAnAP. Missed epochs from the first call were permanently stranded.

Fix: Made removeAP idempotent. If AP is active → deactivate + emit. If already inactive → skip deactivation, proceed with refund-cleanup. Removed unused NotAnAP error.

Status: Resolved in d827d85. Regression tests: test_removeAP_nonAP_noop, test_removeAP_idempotent_cleanupMissedEpoch.

I-01 — Checks-Effects-Interactions in requestMint / requestRedeem

Issue: External safeTransferFrom fired before mapping writes. nonReentrant protected the flow, but canonical CEI pattern preferred.

Fix: Reordered — state mutations + events before the external call.

Status: Resolved in d827d85.

I-02 — ARU did not explicitly inherit IARU

Issue: The ARU token implemented the IARU interface methods but didn't declare is IARU. Interface drift could go undetected until runtime.

Fix: Added is IARU to the inheritance chain, plus explicit override(ERC20Burnable, IARU) on burn / burnFrom to resolve the public-vs-external visibility ambiguity between the OZ parent and the interface.

Status: Resolved in d827d85.

Additional hardening (not findings, but shipped alongside)

  • M-01 mitigation: Added treasuryAllowance() view returning the live USDC allowance from treasury to the manager. Off-chain monitoring alerts when this drops below expected threshold — catches missed re-approvals after treasury rotation.
  • Oracle M-02: ARUPriceOracle.setPrice reverts during large price jumps exceeding maxPriceChangeBps. Acknowledged as intended circuit breaker. Runbook: admin temporarily expands maxPriceChangeBps via setMaxPriceChangeBps during catastrophic NAV shifts to un-stick the relayer.

Test coverage at v1 audit closure

107 tests green across:

  • ARU.t.sol — token, timelocks, permit, transfer, mint/burn
  • ARUMintRedeemManager.t.sol — full epoch flow, settlement, AP registry, pause/freeze, refunds, idempotent removeAP, invariant tests
  • ARUPriceOracle.t.sol — price updates, rate-of-change, staleness strict + non-strict, AggregatorV3 compliance

(The current v2 suite — ARU.t.sol, ARUPriceOracle.t.sol, ARUSubscriptionManager.t.sol — is 113 tests green as verified in the NM-0902 report above.)

Why v2 superseded v1

The v1 contracts reached production-ready with the above findings resolved. But the epoch model's 24h batching creates structural stress during market volatility:

  • Demand surges can't be absorbed mid-epoch — premium widens on secondary markets until next settlement
  • Redemption pressure creates a reflexive discount for up to 20h — classic peg-break dynamic

v2 (per-request, per-AP price-bound) addresses both by decoupling settlement latency from a fixed window. See Per-Request Flows for the active design.

v1 ARUMintRedeemManager has since been removed from the codebase (commit 1153227); the audited production system is v2-only. Its review history is retained above for the audit trail.