|
| 1 | +# CLAUDE.md |
| 2 | + |
| 3 | +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. |
| 4 | + |
| 5 | +## Project Overview |
| 6 | + |
| 7 | +stOLAS is an ERC4626 liquid staking vault for OLAS tokens. Users deposit OLAS on L1 (Ethereum) and receive stOLAS tokens. The deposited OLAS is bridged to L2 chains (Gnosis, Base, Mode) for active staking in the Autonolas service ecosystem. Rewards flow back from L2 to L1, increasing the stOLAS price-per-share. |
| 8 | + |
| 9 | +## Build & Test Commands |
| 10 | + |
| 11 | +```bash |
| 12 | +make install # Install all dependencies (poetry, forge, yarn) |
| 13 | +make build # forge build |
| 14 | +make fmt # forge fmt && forge fmt --check |
| 15 | +make lint # solhint on contracts/**/*.sol |
| 16 | +make tests # forge test -vvv |
| 17 | +make tests-hardhat # Hardhat JS tests (temporarily renames LzOracle.sol) |
| 18 | +make tests-coverage # forge coverage -vvvv |
| 19 | + |
| 20 | +# Run a single Forge test file |
| 21 | +forge test --match-path "test/LiquidStaking.t.sol" -vv |
| 22 | + |
| 23 | +# Run a single Forge test function |
| 24 | +forge test --match-test "testDepositAndStake" -vvv |
| 25 | + |
| 26 | +# Hardhat test variants |
| 27 | +npm run test:hardhat # Full JS test suite |
| 28 | +npm run test:fast # Optimized subset |
| 29 | +npm run test:original # Original comprehensive suite |
| 30 | +``` |
| 31 | + |
| 32 | +**Note:** Hardhat tests temporarily rename `LzOracle.sol` to avoid compilation conflicts. The npm scripts handle this automatically. |
| 33 | + |
| 34 | +## Solidity Configuration |
| 35 | + |
| 36 | +- **Compiler:** 0.8.30, optimizer enabled (1M runs), viaIR, EVM target: Prague |
| 37 | +- **Source directory:** `contracts/` |
| 38 | +- **Key remappings** (in `foundry.toml`): `@openzeppelin`, `@solmate`, `@registries`, `@layerzerolabs`, `@gnosis.pm` |
| 39 | + |
| 40 | +## Architecture |
| 41 | + |
| 42 | +### L1 Contracts (`contracts/l1/`) |
| 43 | + |
| 44 | +| Contract | Role | |
| 45 | +|---|---| |
| 46 | +| **stOLAS** | ERC4626 vault. Tracks `stakedBalance`, `vaultBalance`, `reserveBalance`. PPS = totalReserves / totalSupply | |
| 47 | +| **Depository** | Routes deposits to L2 staking models. Manages model lifecycle (Active/Inactive/Retired) and product types (Alpha/Beta/Final) | |
| 48 | +| **Treasury** | Withdrawal requests via ERC6909 tokens. Triggers L2 unstaking if vault liquidity is insufficient | |
| 49 | +| **Distributor** | Receives bridged rewards. Splits between veOLAS lock and stOLAS vault top-up | |
| 50 | +| **Lock** | veOLAS management for governance voting power | |
| 51 | +| **UnstakeRelayer** | Receives OLAS from retired L2 staking models, returns to stOLAS reserve | |
| 52 | + |
| 53 | +### L2 Contracts (`contracts/l2/`) |
| 54 | + |
| 55 | +| Contract | Role | |
| 56 | +|---|---| |
| 57 | +| **StakingManager** | Orchestrates service deployment/staking. Creates ActivityModule BeaconProxy instances per service | |
| 58 | +| **ExternalStakingDistributor** | Manages external staking on third-party staking proxies. Deploys services (creates Safe multisigs with self as module), stakes/unstakes/re-stakes them, claims and distributes rewards (split between Collector, protocol, and curating agent per configurable factors). Supports V1 (rewards on multisig) and V2 (rewards on contract) staking types. Access-controlled via owner, whitelisted managing agents (for unstakes), and per-proxy curating agents guarded by staking guards. Receives OLAS deposits from L2 staking processor and handles withdraw/unstake requests back through Collector | |
| 59 | +| **StakingTokenLocked** | Restricted StakingToken that only allows StakingManager as staker | |
| 60 | +| **ActivityModule** | Per-service BeaconProxy. Verifies liveness, shields staking funds, triggers reward claims | |
| 61 | +| **Collector** | Collects L2 rewards and bridges to L1 (REWARD→Distributor, UNSTAKE→Treasury, UNSTAKE_RETIRED→UnstakeRelayer) | |
| 62 | + |
| 63 | +### Bridging (`contracts/l1/bridging/`, `contracts/l2/bridging/`) |
| 64 | + |
| 65 | +- LayerZero V2 for cross-chain messaging |
| 66 | +- Chain-specific processors: `GnosisDepositProcessorL1`/`L2`, `BaseDepositProcessorL1`/`L2`, `DefaultDepositProcessorL1`/`L2` |
| 67 | +- `LzOracle` for LayerZero-driven staking model management |
| 68 | + |
| 69 | +### Core Patterns |
| 70 | + |
| 71 | +- **Proxy architecture:** UUPS-style via `Implementation.sol` (owner management + upgrade logic) and `Beacon.sol` (BeaconProxy for ActivityModules) |
| 72 | +- **ERC standards:** ERC4626 (vault), ERC6909 (withdrawal tickets), ERC721 (service NFTs from Autonolas registry) |
| 73 | +- **Libraries:** Solmate (ERC4626, ERC6909, ERC721), OpenZeppelin (security utilities), Autonolas Registries (staking infra) |
| 74 | + |
| 75 | +### Cross-Chain Flow |
| 76 | + |
| 77 | +``` |
| 78 | +Stake: Depository (L1) → DepositProcessor → Bridge → StakingProcessorL2 → StakingManager (L2) |
| 79 | +Rewards: Collector (L2) → Bridge → Distributor (L1) → stOLAS |
| 80 | +Unstake: Collector (L2) → Bridge → Treasury/UnstakeRelayer (L1) → stOLAS |
| 81 | +``` |
| 82 | + |
| 83 | +## Deployment |
| 84 | + |
| 85 | +- Scripts in `scripts/deployment/` follow numbered sequences: `deploy_l1_01_*` through `deploy_l1_15_*`, `deploy_l2_01_*` through `deploy_l2_09_*` |
| 86 | +- Configuration scripts: `script_l1_*`, `script_l2_*` for post-deployment setup |
| 87 | +- Contract addresses: `doc/configuration.json` |
| 88 | +- Finalized ABIs: `abis/0.8.30/` |
| 89 | +- Static audit: `./scripts/deployment/script_static_audit.sh eth_mainnet NETWORK_mainnet` |
| 90 | + |
| 91 | +## ERC4626 Caveat |
| 92 | + |
| 93 | +`deposit` is meant to be called only via **Depository**, and `redeem` only via **Treasury**. The `mint`/`withdraw` functions are non-standard and not for external use. |
| 94 | + |
| 95 | +## Audit Findings & Resolutions |
| 96 | + |
| 97 | +The project has undergone 9 internal audits (`audits/audit1` through `audits/audit9`) and 1 external audit (CODESPECT). |
| 98 | + |
| 99 | +### audit8 (2026-03-08) — all informational, all fixed |
| 100 | +- **INFO-1**: Treasury `requestToWithdraw` didn't forward `msg.value` to unstake calls → Fixed: validates and forwards ETH correctly |
| 101 | +- **INFO-2**: Distributor `_increaseLock` left dangling OLAS approval on failure → Fixed: resets approval to 0 |
| 102 | +- **INFO-3**: ERC6909 withdrawal tokens are transferable → By design |
| 103 | +- **INFO-4**: Permissionless trigger functions → By design |
| 104 | + |
| 105 | +### audit9 (2026-03-18) — 5 Low, 5 Informational, all resolved |
| 106 | +- **L-1**: `reStake` used dead `mapCuratingAgents` mapping → Fixed: uses `mapServiceIdCuratingAgents[serviceId]` |
| 107 | +- **L-2**: `stOLAS.initialize()` no access control → By design (atomic deployment) |
| 108 | +- **L-3**: `DefaultDepositProcessorL1.drain()` sends ETH to `address(0)` → Fixed: removed `drain()` entirely (ETH cannot get stuck) |
| 109 | +- **L-4**: Distributor dangling approval → Fixed (same as audit8 INFO-2) |
| 110 | +- **L-5**: `StakingTokenLocked.maxNumInactivityPeriods` unused → By design (backward compatibility) |
| 111 | +- **INFO-1**: `setCuratingAgents` `stakingHash` in loop → Fixed: moved before loop |
| 112 | +- **INFO-2**: `unstakeAndWithdraw` missing entry validation → Fixed: added zero-checks at entry |
| 113 | +- **INFO-3**: `claim()` permissionless → By design |
| 114 | +- **INFO-4**: `unstakeRetired()` permissionless → By design |
| 115 | +- **INFO-5**: `LzOracle._lzReceive` trusts LZ Read → By design |
| 116 | + |
| 117 | +## Modified Contracts (not yet re-deployed) |
| 118 | + |
| 119 | +Contracts that have been modified but not yet re-deployed are tracked in |
| 120 | +[`MODIFIED_CONTRACTS.md`](MODIFIED_CONTRACTS.md). Keep that file in sync when changing a |
| 121 | +deployed contract, and clear entries once re-deployed. |
0 commit comments