# 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)              |


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.iaero.finance/technical-documentation/audit-epochstakingdistributor-stiaero.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
