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 − beforewith explicit non‑regression and> 0checks.Added Pausable; critical functions (
stake,unstake,exit,notify*,claim*,backfillReceipts) are gated withwhenNotPaused.Strengthened receipt‑token onboarding:
setReceiptTokennow requires a code‑bearing contract, forbids setting toiAERO, and can be frozen.Wider checkpoint timestamps:
uint64 ts(fromuint48) 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
stiAEROas 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) ornotifyRewardForEpoch(token, specificEpoch). ERC‑20 funding uses “pull” with balance‑delta to accommodate fee‑on‑transfer tokens; ETH usesmsg.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
stiAEROis 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, thenpreviewClaimreturns 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
stiAEROroles (MINTER/BURNER) are not granted to the distributor, staking or unstaking will revert. If the distributor’sowneris an EOA, key risk increases.Recommendation: Use a multisig for both
DEFAULT_ADMIN_ROLEonstiAEROandOwnableowner on the distributor; wire roles first, then callsetReceiptToken. Consider freezing pointer after validation.Status: Mitigated operationally (deployment checklist provided).
Low
L‑1: Conservative token recovery can withdraw reward tokens
Description:
recoverERC20is 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/notifyBatchaccept 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:
whenNotPausedprotects claim functions as well.Recommendation: If business preference is to allow claims while paused, remove
whenNotPausedfrom 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_claimto avoid nested guards. Transfers occur after state updates.Funding correctness: ERC‑20 path computes
received = after − beforeand requiresafter ≥ beforeandreceived > 0. ETH path requiresmsg.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 mintsstiAERO1:1, unstaking burns 1:1.exitburns full balance.Ownership:
Ownable(msg.sender)in constructor; recommended to transfer to multisig post‑deploy.
Operational Guidance (How to Use)
Deployment (Base)
Deploy
EpochStakingDistributor(iAERO).Deploy
StiAERO(admin=multisig).From the admin (multisig), grant roles to the distributor:
grantRole(MINTER_ROLE, <Distributor>)grantRole(BURNER_ROLE, <Distributor>)
On distributor, set receipt token:
setReceiptToken(<StiAERO>). (Optional:freezeReceiptToken().)Allow funder:
setAllowedFunder(<RewardsHarvester>, true).Transfer ownership of distributor to multisig (recommended).
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)orpreviewClaimsForEpoch.
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 ofbalanceOfacross users (not tracked on‑chain; verify off‑chain).
Test Recommendations
Property tests:
Sum of
previewClaimacross users ≤ funded amount.Receipt mint/burn matches delta in
balanceOf.Funding with FOT token yields
received > 0and claims matchreceived.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
nonReentrantand 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:
StiAEROtoken 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_ROLEon 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:
stiAEROis a regular ERC‑20 (transferable,permitsupported). 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
uncheckedarithmetic in ERC‑20 funding; added explicit post‑transfer checks.✔️ Added Pausable to all critical flows.
✔️ Hardened
setReceiptToken(code length check; forbidiAERO; optional freezing).✔️ Widened checkpoint timestamp to
uint64.✔️ Added conservative
recoverERC20with exclusions.✔️ Used internal
_claimto avoid nestingnonReentrant.
With these changes, the design is sound and production‑ready for Base, assuming operational best practices:
Run with multisig admin/owner.
Set
allowedFundersto the Harvester only.Handle zero‑supply epochs at the operations layer (fund next epoch or accept that bucket as 0).
Avoid using
recoverERC20except for obvious dust; do not sweep active reward tokens.
Appendix — Function Access Map (Distributor)
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)
Last updated