|
| 1 | +# Security Audit Report — olas-lst (stOLAS) |
| 2 | + |
| 3 | +**Date**: 2026-03-08 |
| 4 | +**Scope**: All contracts in `contracts/` (excluding `lib/`, `test/`, `contracts/l1/concept/`, `contracts/test/`) |
| 5 | +**Lines of Code**: 8,086 LOC across 29 contracts |
| 6 | + |
| 7 | +--- |
| 8 | + |
| 9 | +## Executive Summary |
| 10 | + |
| 11 | +The stOLAS liquid staking protocol was audited following the 5-phase security audit methodology. The codebase implements a cross-chain ERC4626 vault system with L1 (Ethereum) deposits/withdrawals and L2 (Gnosis/Base) staking operations. |
| 12 | + |
| 13 | +**Result: No bounty-qualifying vulnerabilities found.** |
| 14 | + |
| 15 | +The protocol demonstrates mature defensive coding patterns: |
| 16 | +- Internal accounting model (totalReserves) immune to donation-based price manipulation |
| 17 | +- Time-delayed withdrawals preventing flash loan and sandwich attacks |
| 18 | +- Per-contract reentrancy guards (transient storage on L1, persistent on L2) |
| 19 | +- Strict access control with one-caller-per-function pattern on stOLAS |
| 20 | +- Optimistic cross-chain accounting with recovery mechanisms for failed bridge messages |
| 21 | + |
| 22 | +4 informational observations were documented. The protocol has undergone 7 internal and 1 external audit (CODESPECT), and the code quality reflects this maturity. |
| 23 | + |
| 24 | +--- |
| 25 | + |
| 26 | +## Scope |
| 27 | + |
| 28 | +### In-Scope Contracts |
| 29 | + |
| 30 | +**L1 (Ethereum) — 2,633 LOC** |
| 31 | +| Contract | LOC | Description | |
| 32 | +|----------|-----|-------------| |
| 33 | +| stOLAS.sol | 378 | ERC4626 vault, main entry point | |
| 34 | +| Depository.sol | 1,139 | Staking model management, cross-chain bridge hub | |
| 35 | +| Treasury.sol | 303 | Withdrawal queue, ERC6909 claim tokens | |
| 36 | +| Distributor.sol | 148 | Reward split (veOLAS lock + vault) | |
| 37 | +| Lock.sol | 287 | veOLAS lock management, governance proxy | |
| 38 | +| UnstakeRelayer.sol | 80 | Retired model unstake relay | |
| 39 | + |
| 40 | +**L1 Bridging — 738 LOC** |
| 41 | +| Contract | LOC | Description | |
| 42 | +|----------|-----|-------------| |
| 43 | +| DefaultDepositProcessorL1.sol | 182 | Abstract bridge base | |
| 44 | +| BaseDepositProcessorL1.sol | 116 | Optimism/Base bridge | |
| 45 | +| GnosisDepositProcessorL1.sol | 62 | Gnosis AMB bridge | |
| 46 | +| LzOracle.sol | 378 | LayerZero lzRead oracle (NOT currently used) | |
| 47 | + |
| 48 | +**L2 (Gnosis/Base) — 3,851 LOC** |
| 49 | +| Contract | LOC | Description | |
| 50 | +|----------|-----|-------------| |
| 51 | +| StakingManager.sol | 556 | Service orchestration | |
| 52 | +| ExternalStakingDistributor.sol | 1,123 | External staking management | |
| 53 | +| StakingTokenLocked.sol | 848 | Locked staking instance | |
| 54 | +| ActivityModule.sol | 342 | Service activity + claim | |
| 55 | +| Collector.sol | 406 | Reward collection + bridging | |
| 56 | +| StakingHelper.sol | 77 | View helper | |
| 57 | +| MultisigGuard.sol | 216 | Safe guard for externals | |
| 58 | +| ModuleActivityChecker.sol | 33 | Activity nonce checker | |
| 59 | + |
| 60 | +**L2 Bridging — 679 LOC** |
| 61 | +| Contract | LOC | Description | |
| 62 | +|----------|-----|-------------| |
| 63 | +| DefaultStakingProcessorL2.sol | 473 | Abstract bridge base | |
| 64 | +| BaseStakingProcessorL2.sol | 113 | Optimism/Base bridge | |
| 65 | +| GnosisStakingProcessorL2.sol | 93 | Gnosis bridge | |
| 66 | + |
| 67 | +**Infrastructure — 255 LOC** |
| 68 | +| Contract | LOC | Description | |
| 69 | +|----------|-----|-------------| |
| 70 | +| Beacon.sol | 62 | Beacon pattern | |
| 71 | +| BeaconProxy.sol | 60 | EIP-1967 beacon proxy | |
| 72 | +| Proxy.sol | 73 | UUPS-style proxy | |
| 73 | +| Implementation.sol | 60 | Base implementation with owner | |
| 74 | + |
| 75 | +### Out of Scope |
| 76 | +- `lib/` dependencies (autonolas-registries, LayerZero, OpenZeppelin, solmate) |
| 77 | +- `contracts/test/` mock contracts |
| 78 | +- `contracts/l1/concept/` experimental contracts |
| 79 | +- Owner/multisig actions (trusted role per CLAUDE.md) |
| 80 | +- LzOracle.sol (confirmed not currently used) |
| 81 | + |
| 82 | +--- |
| 83 | + |
| 84 | +## Findings |
| 85 | + |
| 86 | +### Critical: 0 |
| 87 | +### High: 0 |
| 88 | +### Medium: 0 |
| 89 | +### Low: 0 |
| 90 | + |
| 91 | +### Informational: 4 |
| 92 | + |
| 93 | +#### INFO-1: Missing ETH Forwarding in Treasury.requestToWithdraw |
| 94 | +| | | |
| 95 | +|---|---| |
| 96 | +| **Location** | Treasury.sol:220-221, 227-228 | |
| 97 | +| **Description** | `requestToWithdraw` is `payable` but doesn't forward `msg.value` to `Depository.unstake`/`unstakeExternal` calls | |
| 98 | +| **Impact** | None currently — Optimism and Gnosis bridges don't require ETH for L1→L2 messages. If a bridge requiring ETH is added, unstaking from Treasury would fail. | |
| 99 | +| **Recommendation** | Add `{value: ...}` forwarding or document that future bridges must not require ETH for L1→L2 UNSTAKE messages | |
| 100 | + |
| 101 | +#### INFO-2: Dangling OLAS Approval in Distributor |
| 102 | +| | | |
| 103 | +|---|---| |
| 104 | +| **Location** | Distributor.sol:75 | |
| 105 | +| **Description** | If `Lock.increaseLock()` fails via low-level call, OLAS approval to Lock remains until next `distribute()` call | |
| 106 | +| **Impact** | None — Lock is immutable trusted address. Approval overwritten on next distribute(). | |
| 107 | +| **Recommendation** | Consider resetting approval to 0 after failed lock call | |
| 108 | + |
| 109 | +#### INFO-3: ERC6909 Withdrawal Tokens Are Transferable |
| 110 | +| | | |
| 111 | +|---|---| |
| 112 | +| **Location** | Treasury.sol:190, solmate ERC6909 | |
| 113 | +| **Description** | Withdrawal claim tokens can be transferred. New holder can finalize withdrawal after delay. | |
| 114 | +| **Impact** | Feature, not bug. Enables secondary market for withdrawal claims. | |
| 115 | +| **Recommendation** | Document this behavior for users | |
| 116 | + |
| 117 | +#### INFO-4: Multiple Permissionless Trigger Functions |
| 118 | +| | | |
| 119 | +|---|---| |
| 120 | +| **Location** | Depository.sol:763,829; Distributor.sol:125; UnstakeRelayer.sol:60; Collector.sol:317; ActivityModule.sol:303 | |
| 121 | +| **Description** | These functions (unstakeRetired, closeRetiredStakingModels, distribute, relay, relayTokens, claim) can be called by anyone. | |
| 122 | +| **Impact** | None — all are designed as permissionless triggers. Cannot cause fund loss when called by untrusted parties. | |
| 123 | +| **Recommendation** | N/A — by design | |
| 124 | + |
| 125 | +--- |
| 126 | + |
| 127 | +## Methodology |
| 128 | + |
| 129 | +### Phase 1: Architecture Map |
| 130 | +- Mapped 29 in-scope contracts across L1, L2, bridging, and infrastructure layers |
| 131 | +- Documented inheritance hierarchy, inter-contract call graph, access control matrix |
| 132 | +- Identified 10 priority attack surface areas |
| 133 | + |
| 134 | +### Phase 2: Invariant Identification |
| 135 | +- Verified 10 core invariants (all holding) |
| 136 | +- Disproved 8 initial candidate findings through code verification |
| 137 | +- Identified 8 candidates for Phase 3 deep investigation |
| 138 | + |
| 139 | +### Phase 3: Attack Discovery |
| 140 | +- Investigated all 8 Phase 2 candidates — all disproved |
| 141 | +- Ran 16-item EVM checklist — all SAFE |
| 142 | +- Ran 8-item DeFi attack pattern checklist — all SAFE |
| 143 | +- Applied disprove-first methodology to every finding |
| 144 | + |
| 145 | +### Coverage |
| 146 | + |
| 147 | +| Category | Items Checked | |
| 148 | +|----------|--------------| |
| 149 | +| Phase 2 candidates | 8/8 investigated | |
| 150 | +| EVM patterns | 16/16 checked | |
| 151 | +| DeFi patterns | 8/8 checked | |
| 152 | +| Invariants verified | 10/10 | |
| 153 | +| False positives disproved | 17 | |
| 154 | +| Informational findings | 4 | |
| 155 | + |
| 156 | +--- |
| 157 | + |
| 158 | +## Key Architectural Observations |
| 159 | + |
| 160 | +### Strengths |
| 161 | +1. **Internal accounting model**: `totalAssets()` returns `totalReserves` (not `balanceOf`), making the vault immune to donation-based attacks (ERC4626 inflation, share price manipulation) |
| 162 | +2. **Withdrawal delay**: Time-delayed withdrawals via Treasury prevent flash loans, sandwich attacks, and cross-chain arbitrage |
| 163 | +3. **Access control isolation**: Each stOLAS function has exactly one authorized caller — no cross-role attack vectors |
| 164 | +4. **Graceful degradation**: Distributor handles Lock failure gracefully (all rewards go to vault); L2 bridge queues failed operations for retry |
| 165 | +5. **Rounding direction**: Both deposit and redeem use `mulDivDown` (favor vault), preventing rounding extraction |
| 166 | + |
| 167 | +### Design Trade-offs (Not Vulnerabilities) |
| 168 | +1. **Optimistic cross-chain accounting**: L1 updates balances before L2 confirms. Relies on operational liveness for recovery. |
| 169 | +2. **No virtual shares**: stOLAS doesn't implement OpenZeppelin's virtual shares offset. Mitigated by depository-only deposit access control. |
| 170 | +3. **ERC4626 non-standard functions**: `mint()` and `withdraw()` revert. May confuse integrators. |
| 171 | + |
| 172 | +--- |
| 173 | + |
| 174 | +*Audit conducted using the Security Audit Playbook v2.8. All findings were verified using disprove-first methodology with exact file:line references.* |
0 commit comments