> For the complete documentation index, see [llms.txt](https://docs.iaero.finance/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.iaero.finance/technical-documentation/audit-epochstakingdistributor-stiaero.md).

# Audits Epoch Staking Distributor & stiAERO

**Auditor:** Independent Security Review **Date:** September 2025 **Network:** Base Mainnet

***

## Audit 1 — EpochStakingDistributor

**Contract Address:** `0x781A80fA817b5a146C440F03EF8643f4aca6588A`

### Executive Summary

**Risk rating:** **Low–Medium overall** The contract implements a **snapshot‑per‑epoch** reward distribution for iAERO stakers. It does **not** maintain or iterate any token list, which keeps `stake`/`unstake` gas predictable. Rewards are accounted by **(token, epoch)** and claimed by users directly. The contract follows safe interaction patterns (**checks‑effects‑interactions**, `nonReentrant`) and now includes **Pausable**, tighter **funding arithmetic**, and **safer receipt‑token linkage**.

### What changed / fixed

* **Removed unchecked arithmetic** in ERC‑20 funding; now we compute `received = after − before` with explicit non‑regression and `> 0` checks.
* **Added Pausable;** critical functions (`stake`, `unstake`, `exit`, `notify*`, `claim*`, `backfillReceipts`) are gated with `whenNotPaused`.
* **Strengthened receipt‑token onboarding:** `setReceiptToken` now requires a code‑bearing contract, forbids setting to `iAERO`, and can be **frozen**.
* **Wider checkpoint timestamps:** `uint64 ts` (from `uint48`) while keeping checkpoints to one slot (`uint64 ts | uint192 value`).
* **Conservative ERC‑20 recovery:** owner can only recover **non‑iAERO** and **non‑receipt** tokens (with clear warning; see “Operations & Risks”).
* **Internal claim path** for batch claims avoids nested `nonReentrant`.

### Scope & Methodology

* **In‑scope files:** Epoch‑snapshot distributor contract provided in the last version you approved.
* **Out‑of‑scope:** Vault, Harvester, Aerodrome Voter, token prices/oracles, third‑party protocols using `stiAERO` as collateral.
* **Approach:** Manual line‑by‑line review, invariants & state‑machine reasoning, reentrancy/authorization analysis, economic checks for reward splits, edge‑case exploration (zero‑supply epochs, FOT tokens), gas & grief‑ing considerations.

### System Overview

* **Stake/Unstake:** Users stake iAERO into the distributor; (optionally) a **receipt token** (`stiAERO`) is minted/burned 1:1 with staked iAERO.
* **Epoch model:** Rewards are funded to **(token, epoch)** buckets. Payout shares are computed from **epoch‑start snapshots** of user balance and total supply.
* **Funding:** `notifyRewardAmount(token, amount)` (current epoch) or `notifyRewardForEpoch(token, specificEpoch)`. ERC‑20 funding uses “pull” with **balance‑delta** to accommodate fee‑on‑transfer tokens; ETH uses `msg.value`.
* **Claiming:** Users call `claim(token, epoch)` (or batched variants) to pull rewards. The distributor transfers directly to the user.
* **No token enumeration:** The contract never stores an “active set” of reward tokens; all loops are bounded by the caller’s input (capped at 50 items per batch).

### Roles & Permissions

* **Owner:** pause/unpause; set receipt token / freeze pointer; set allowed funders; perform conservative `recoverERC20`.
* **Allowed funders:** call `notify*` functions.
* **Anyone:** stake/unstake (user), claim personal rewards.

### Threat Model & Trust Assumptions

* **Trusted:** Contract owner/multisig; allowed funders (e.g., your Harvester).
* **Untrusted:** Users; arbitrary ERC‑20 tokens used as rewards; external protocols where `stiAERO` is used as collateral.
* **Dependencies:** OpenZeppelin (v5) `Ownable`, `ReentrancyGuard`, `Pausable`, `SafeERC20`, `Math`.

### Findings

#### Critical / High

* **None observed.**

#### Medium

**M‑1: Zero‑supply epoch ⇒ permanent 0 claims for that epoch**

* **Description:** If `totalSupplyAtEpochStart(epoch) == 0`, then `previewClaim` returns 0 for that epoch forever (even if users stake mid‑epoch). This is **by design** for snapshot fairness but leaves funds stranded in that epoch bucket.
* **Impact:** Operational. Rewards for such epochs do not flow to users.
* **Recommendation:** Handle at the **Harvester/ops** layer—if snapshot is 0 for intended epoch, fund the **next** epoch. Optionally add an owner‑only “roll‑forward” function to move untouched rewards to a later epoch.
* **Status:** **Accepted by design.** Documented in runbook (see below).

**M‑2: Admin / role mis‑configuration risk**

* **Description:** If `stiAERO` roles (MINTER/BURNER) are not granted to the distributor, staking or unstaking will revert. If the distributor’s `owner` is an EOA, key risk increases.
* **Recommendation:** Use a **multisig** for both `DEFAULT_ADMIN_ROLE` on `stiAERO` and `Ownable` owner on the distributor; wire roles first, then call `setReceiptToken`. Consider freezing pointer after validation.
* **Status:** **Mitigated operationally** (deployment checklist provided).

#### Low

**L‑1: Conservative token recovery can withdraw reward tokens**

* **Description:** `recoverERC20` is intentionally narrow (excludes iAERO & receipt token) but could still remove a token that has funded/claimable balances.
* **Recommendation:** Use only for obvious dust. For a fully safe recovery, a richer liability check would be needed (not recommended due to gas/storage overhead).
* **Status:** **Acknowledged**; keep as emergency escape with procedures.

**L‑2: Batch sizes and user checkpoint growth**

* **Description:** `claimMany`/`notifyBatch` accept up to 50 items; checkpoint arrays grow with user activity.
* **Recommendation:** The caps and binary search are adequate; periodically test gas envelopes.
* **Status:** **Accepted**.

**L‑3: Pausing blocks claims**

* **Description:** `whenNotPaused` protects claim functions as well.
* **Recommendation:** If business preference is to **allow claims while paused**, remove `whenNotPaused` from claim functions.
* **Status:** **Configuration choice**.

### Design Soundness & Invariants

* **No token enumeration:** The distributor never loops “all tokens”; all loops depend on **caller input** (bounded/capped).
* **Reentrancy:** All external state‑mutating functions are `nonReentrant`. Claims use internal `_claim` to avoid nested guards. Transfers occur **after** state updates.
* **Funding correctness:** ERC‑20 path computes `received = after − before` and requires `after ≥ before` and `received > 0`. ETH path requires `msg.value == amount`.
* **Snapshot math:** For user `u`, `payout = rewards[token][epoch] * bal_at_start(u) / supply_at_start`. The sum of payouts over all users ≤ funded amount (flooring/rounding may leave dust).
* **Receipt alignment:** After `setReceiptToken`, staking mints `stiAERO` 1:1, unstaking burns 1:1. `exit` burns full balance.
* **Ownership:** `Ownable(msg.sender)` in constructor; recommended to transfer to multisig post‑deploy.

### Operational Guidance (How to Use)

#### Deployment (Base)

1. **Deploy** `EpochStakingDistributor(iAERO)`.
2. **Deploy** `StiAERO(admin=multisig)`.
3. From the **admin** (multisig), **grant roles** to the distributor:
   * `grantRole(MINTER_ROLE, <Distributor>)`
   * `grantRole(BURNER_ROLE, <Distributor>)`
4. On distributor, **set receipt token**: `setReceiptToken(<StiAERO>)`. (Optional: `freezeReceiptToken()`.)
5. **Allow funder:** `setAllowedFunder(<RewardsHarvester>, true)`.
6. **Transfer ownership** of distributor to multisig (recommended).
7. If there were existing stakes, use `backfillReceipts([users...])` in batches.

#### Routine Operations

* **Funding (Harvester):**
  * Normal: `notifyRewardAmount(token, amount)` → current epoch.
  * Specific epoch: `notifyRewardForEpoch(token, epoch, amount)` (current or previous only).
  * Batch: `notifyRewardAmountsBatch(tokens[], epochs[], amounts[])` (≤ 50 legs).
* **Claiming (User/UI):**
  * Single: `claim(token, epoch)`
  * Many: `claimMany(tokens[], epochs[])` (n tokens for n epochs)
  * Weekly default: `claimLatest(tokens[])` (current + previous)
  * Preview: `previewClaim(user, token, epoch)` or `previewClaimsForEpoch`.
* **Emergency:**
  * `pause()` → blocks stake/unstake/notify/claim/backfill.
  * `unpause()` restores.
  * `recoverERC20(token, to, amount)` only for non‑iAERO/non‑receipt dust.

#### Monitoring & Playbooks

* **Zero‑supply epoch:** If `supplySnapshotAtEpochStart[epoch] == 0`, fund **next epoch** instead.
* **Unclaimed dust:** Due to rounding, small dust may remain. This is acceptable; do not sweep casually.
* **Receipt invariants:** `stiAERO.totalSupply()` should equal the sum of `balanceOf` across users (not tracked on‑chain; verify off‑chain).

### Test Recommendations

* **Property tests:**
  * Sum of `previewClaim` across users ≤ funded amount.
  * Receipt mint/burn matches delta in `balanceOf`.
  * Funding with FOT token yields `received > 0` and claims match `received`.
  * Epoch with zero supply produces zero claims; funding next epoch produces non‑zero claims.
* **Reentrancy tests:** Attempt reentrancy via ERC‑777 style tokens or malicious receipt token; should be blocked by `nonReentrant` and call order.
* **Pause tests:** Verify all protected functions revert when paused.

***

## Audit 2 — StiAERO (Receipt Token)

**Contract Address:** `0x72C135B8eEBC57A3823f0920233e1A90FF4D683D`

### Executive Summary

**Risk rating:** **Low** `StiAERO` is a standard transferable ERC‑20 with `ERC20Permit` and `AccessControl`. The staking distributor is granted `MINTER_ROLE` and `BURNER_ROLE` to mint/burn receipts 1:1 with staked balances. The security posture primarily depends on role governance and the distributor’s correctness.

### Scope & Methodology

* **In‑scope:** `StiAERO` token contract.
* **Approach:** Manual review for ERC‑20 correctness, role gating, and mint/burn semantics.

### System Overview

* **Token:** *Staked iAERO* / `stiAERO` (18 decimals).
* **Roles:**
  * `DEFAULT_ADMIN_ROLE`: manages roles; set to a **multisig** at deployment.
  * `MINTER_ROLE`: granted to distributor.
  * `BURNER_ROLE`: granted to distributor.
* **Mint/Burn:**
  * `mint(to, amount)` only by MINTER.
  * `burn(from, amount)` only by BURNER (no allowance required; distributor burns directly during unstake/exit).

### Findings

#### Critical / High

* **None observed.**

#### Medium

**M‑1: Centralized roles**

* **Description:** Admin can mint/burn if they grant roles to themselves or a compromised address.
* **Recommendation:** Keep `DEFAULT_ADMIN_ROLE` on a **multisig**, avoid keeping MINTER/BURNER on EOAs beyond deployment. Revoke any temporary roles.

#### Low

**L‑1: External dependencies**

* **Description:** Relies on OZ ERC‑20/Permit/AccessControl; well‑audited but requires correct linkage and compiler settings.
* **Recommendation:** Pin OZ version in your build, run compilation with optimizer enabled and consistent settings across contracts.

### Operational Guidance (How to Use)

* **Deployment:** `new StiAERO(admin=multisig)`; immediately grant MINTER/BURNER to distributor.
* **After wiring:** Optionally revoke any roles from the deployer EOA; keep only multisig (admin) and distributor (minter/burner).
* **Usage in UIs/protocols:** `stiAERO` is a regular ERC‑20 (transferable, `permit` supported). Users may deposit it as collateral elsewhere—but they must hold enough to burn when unstaking.

### Fixes & Closing Notes

The combined system (Distributor + `stiAERO`) incorporated the following security improvements before this final audit:

* ✔️ Removed `unchecked` arithmetic in ERC‑20 funding; added explicit post‑transfer checks.
* ✔️ Added **Pausable** to all critical flows.
* ✔️ Hardened `setReceiptToken` (code length check; forbid `iAERO`; optional freezing).
* ✔️ Widened checkpoint timestamp to `uint64`.
* ✔️ Added conservative `recoverERC20` with exclusions.
* ✔️ Used internal `_claim` to avoid nesting `nonReentrant`.

With these changes, the design is **sound and production‑ready** for Base, assuming operational best practices:

* Run with **multisig** admin/owner.
* Set `allowedFunders` to the Harvester only.
* Handle **zero‑supply epochs** at the operations layer (fund next epoch or accept that bucket as 0).
* Avoid using `recoverERC20` except for obvious dust; do not sweep active reward tokens.

***

## Appendix — Function Access Map (Distributor)

| Function                                | Access              | Pausable | Reentrancy | External calls                                         |
| --------------------------------------- | ------------------- | :------: | :--------: | ------------------------------------------------------ |
| `stake`, `stakeFor`                     | public              |    Yes   |     Yes    | ERC‑20 `transferFrom` (iAERO), optional `stiAERO.mint` |
| `unstake`, `exit`                       | public              |    Yes   |     Yes    | optional `stiAERO.burn`, ERC‑20 `transfer` (iAERO)     |
| `notifyRewardAmount`                    | owner/allowedFunder |    Yes   |     Yes    | ERC‑20 `transferFrom` (reward)                         |
| `notifyRewardForEpoch`                  | owner/allowedFunder |    Yes   |     Yes    | ERC‑20 `transferFrom` (reward)                         |
| `notifyRewardAmountsBatch`              | owner/allowedFunder |    Yes   |     Yes    | ERC‑20 `transferFrom` (reward)                         |
| `claim`                                 | public              |    Yes   |     Yes    | ETH transfer or ERC‑20 `transfer` (reward)             |
| `claimMany`, `claimLatest`              | public              |    Yes   |     Yes    | ETH / ERC‑20 transfers                                 |
| `setReceiptToken`, `freezeReceiptToken` | owner               |    n/a   |     n/a    | sets receipt address                                   |
| `backfillReceipts`                      | owner               |    Yes   |     Yes    | `stiAERO.mint`                                         |
| `setAllowedFunder`                      | owner               |    n/a   |     n/a    | —                                                      |
| `pause`, `unpause`                      | owner               |    n/a   |     n/a    | —                                                      |
| `recoverERC20`                          | owner               |    n/a   |     n/a    | ERC‑20 `transfer` (non‑iAERO/non‑receipt)              |
