diff --git a/CLAUDE.md b/CLAUDE.md index d03f72def1..c2bdc252ef 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -11,15 +11,21 @@ Origin DeFi's OTokens monorepo containing smart contracts for: Deployed on Ethereum Mainnet, Base, Arbitrum, Sonic, Plume, Hoodi, and HyperEVM. All smart contract work happens in the `contracts/` directory. +## Toolchain + +**Foundry is the toolchain**: `forge` (driven by the `Makefile`) builds the contracts, runs the test suite, and executes deployments. Hardhat is no longer used for building, testing, or new deployments — it remains only to run the ops task CLI (`tasks/*.js`, wired up in `hardhat.config.js`). The Hardhat test suite (`test/`) and deploy scripts (`deploy/`) are frozen legacy; do not extend either. + ## Setup ```bash cd contracts -cp dev.env .env # Set PROVIDER_URL to Alchemy/Infura endpoint -pnpm i +cp dev.env .env # Set MAINNET_PROVIDER_URL to an Alchemy/Infura endpoint +make install # foundryup (stable), forge soldeer install, install-deps.sh, pnpm i ``` -Key `.env` variables: `PROVIDER_URL`, `SONIC_PROVIDER_URL`, `BASE_PROVIDER_URL`, `BLOCK_NUMBER`, `ACCOUNTS_TO_FUND`. +Key `.env` variables: `MAINNET_PROVIDER_URL` (required), `BASE_PROVIDER_URL`, `ARBITRUM_PROVIDER_URL`, `SONIC_PROVIDER_URL`, `HYPEREVM_PROVIDER_URL`, `BEACON_PROVIDER_URL` (beacon-proof fork tests), and optional `FORK_BLOCK_NUMBER_` pins for Foundry fork tests (unset = latest block; refresh with `make update-fork-blocks`). The Hardhat task CLI resolves its mainnet RPC from `MAINNET_PROVIDER_URL` as well (legacy fallback: `PROVIDER_URL`). + +Real deployments additionally need `DEPLOYER_ADDRESS` and the encrypted `deployerKey` keystore (`cast wallet import deployerKey --interactive`). ### `@oplabs/talos-client` (private, optional) @@ -50,67 +56,109 @@ Keep it that way. Two regressions to avoid: ### Build ```bash -pnpm hardhat compile # Compile changed contracts -pnpm clean && pnpm hardhat compile # Full recompile +make build # forge build everything +make build-contracts # only contracts/ — fastest while iterating +make build-tests-unit # also: build-tests, build-tests-fork, build-tests-smoke, build-scripts ``` +`make` with no target runs `forge fmt scripts/ tests/` and then builds. ### Lint & Format ```bash -pnpm lint # Run all linters (Solidity + JS) -pnpm lint:sol # solhint for Solidity -pnpm lint:js # eslint for JavaScript -pnpm prettier:check # Check formatting -pnpm prettier # Format all files +forge fmt scripts/ tests/ # Foundry-tree Solidity (tests + deploy scripts) +pnpm prettier:sol # contracts/**/*.sol (prettier-plugin-solidity) +pnpm prettier:js # JS (tasks, scripts, utils, legacy test/) +pnpm prettier:ts # TS (tasks/actions, tasks/lib) +pnpm lint # eslint (JS + TS) and solhint +make lint-imports # forge lint: unused imports in scripts/ and tests/ ``` -### Unit Tests +### Tests (Foundry) ```bash -pnpm test # Mainnet unit tests -pnpm test:base # Base network unit tests -pnpm test:sonic # Sonic network unit tests -pnpm test:coverage # Mainnet unit tests with coverage -pnpm test test/**/FILE_NAME.js # Running a specific test file +make test-unit # tests/unit/ — mocked, no RPC needed +make test-fork-mainnet # per chain: test-fork-mainnet|base|hyperevm|arbitrum; make test-fork for all +make test-smoke-mainnet # per chain: test-smoke-mainnet|base|hyperevm; make test-smoke for all +make test-f-testSwap # tests matching a function name +make test-c-OETHVault # tests matching a contract name +make coverage # lcov; make coverage-html for a browsable report +make gas # forge test --gas-report ``` -### Fork Tests (require `PROVIDER_URL` in `.env`) +The `make test-*` targets rebuild the relevant trees first. Bare `forge test` does **not**: tests deploy contracts with `vm.deployCode`, which reads compiled artifacts, so after changing contract source you must `forge build contracts/` or a bare `forge test` silently tests stale code. + +Fork and smoke tests fork the chain from the `*_PROVIDER_URL` variables and pin blocks via `FORK_BLOCK_NUMBER_`. Test conventions (unit vs fork vs smoke, concrete vs fuzz, interface-only testing) are documented in `tests/README.md` — read it before writing tests. + +### Legacy Hardhat suite (`test/`, frozen) + +`test/**/*.js` and `test/**/*.fork-test.js` are the pre-migration Hardhat tests. They no longer run in CI and must not be extended — write Foundry tests in `tests/` instead. Coverage gaps between the two suites are tracked in `docs/foundry-migration-gap-analysis.md` (per-test inventory: `docs/hardhat-test-inventory.md`). + +### Hardhat (ops task CLI only) ```bash -# Option 1: Fork each time (like CI) -pnpm test:fork # All mainnet fork tests -pnpm test:fork -- test/strategies/foo.fork-test.js # Single fork test file - -# Option 2: Nested forking (faster for dev iteration) -FORK=true pnpm run node # Terminal 1: start forked node with deployments -pnpm test:fork # Terminal 2: tests reuse running node - -# Other networks -pnpm test:arb-fork -pnpm test:base-fork -pnpm test:sonic-fork -pnpm test:hol-fork +npx hardhat --network mainnet # ops tasks from tasks/*.js +pnpm run node # Hardhat fork node, for running tasks against a local fork +pnpm node:anvil # anvil mainnet fork (used by make deploy-local); node:anvil:base etc. +pnpm action # Talos actions runner (tsx tasks/run.ts — viem, not Hardhat) ``` ### Useful Options ```bash -export DEBUG=origin:* # Enable all debug logging -export REPORT_GAS=true # Show gas usage in test output -export CONTRACT_SIZE=true # Show contract sizes after compile +export DEBUG=origin:* # Debug logging in tasks and JS tooling ``` ## Workflow Guidance - Run repo commands from `contracts/` for smart contract work. -- After making code changes, run Prettier before finishing. -- For JS edits under `contracts/`, run `pnpm prettier:js`. -- For Solidity edits under `contracts/`, run `pnpm prettier:sol`. -- If both JS and Solidity files changed, run both commands. -- Prefer the smallest relevant verification after edits. +- After editing Solidity under `contracts/contracts/`, run `pnpm prettier:sol`. +- After editing Solidity under `scripts/` or `tests/`, run `forge fmt scripts/ tests/`. +- For JS edits run `pnpm prettier:js`; for TS under `tasks/`, `pnpm prettier:ts`. +- Prefer the smallest relevant verification after edits (targeted `make build-*`, a single `make test-c-...`). - Do not reformat or modify unrelated files just to satisfy style. - Do not fix unrelated failing tests or lint issues unless explicitly asked. +## Core contract stability (Vault & Token) — DESIGN PRINCIPLE + +**Avoid changing the core contracts — the Vaults (`contracts/vault/`) and the +OTokens (`contracts/token/`) — whenever the same outcome can be reached without +touching them.** Keeping the core effectively frozen is a deliberate Origin +value, not an accident of the codebase. + +Why: these contracts hold all user funds and all rebasing accounting. Every +change to them carries storage-layout risk, needs a full implementation upgrade +plus a governance proposal and timelock, invalidates prior audit coverage, and +adds bug surface to the most critical code in the protocol. The peripheral +contracts carry none of that. + +**Before proposing a Vault or OToken change, work through these first and say in +the PR description which you ruled out and why:** + +1. **Re-point an existing role.** `setStrategistAddr`, `setOperatorAddr`, + `setTrusteeAddress` etc. can often produce the desired permission structure + with a transaction instead of an upgrade. +2. **Put a Safe module on an existing multisig.** A module can grant scoped, + delegable access to functions the multisig already has the right to call — + including to third parties — with no core change. Scope modules by + `(target, selector)` pair, never by target alone. +3. **Add a peripheral contract** (harvester, dripper, zapper, automation + contract) that composes with the existing core interface. + +Only when none of those work should the core change be made — and then it should +be as small as possible. + +Note the protocol-wide reach of the role addresses when evaluating option 1: +`IVault.strategistAddr()` is the strategist source of truth for `OUSD.sol`, +`InitializableAbstractStrategy` (so every strategy), the Curve/Aerodrome/Algebra +AMO strategies, `ValidatorRegistrator`, `SonicValidatorDelegator` and +`FixedRateDripper`. Re-pointing it is a cheap transaction but a wide blast +radius — enumerate the affected call sites before recommending it. + +Worked example: PR #2973 added an `adminAddr` role to `VaultAdmin`/`VaultStorage` +(storage-layout change + 3 vault upgrades) to make unpause a 5/8 action. The same +result was achievable with no core change by re-pointing the Vault's strategist +to the 5/8 Admin Safe and hosting a Safe module for the 2/8 and the operator EOA. + ## Architecture ### Core Pattern: Upgradeable Proxy Contracts -All major contracts use the OpenZeppelin upgradeable proxy pattern. Each has a `*Proxy` contract (minimal proxy) pointing to an implementation. Proxies are deployed via `hardhat-deploy` scripts in `deploy/`. +All major contracts use the OpenZeppelin upgradeable proxy pattern. Each has a `*Proxy` contract (minimal proxy) pointing to an implementation. New deployments and upgrades go through the Foundry deployment framework in `scripts/deploy/`; the `deploy/` hardhat-deploy scripts are the historical ledger. ### Vaults (Central Component) Vaults (`contracts/vault/`) are the core of each OToken. They handle: @@ -119,11 +167,9 @@ Vaults (`contracts/vault/`) are the core of each OToken. They handle: - Rebalancing via `allocate()` - Yield accounting via `rebase()` -Each chain/token has its own vault: `OUSDVault`, `OETHVault`, `OETHBVault` (Base), `OETHSVault` (Sonic) and `OETPVault` (Plume). `OETPVault` is being shut down and will be removed from the repo after all funds are withdrawn. +Each chain/token has its own vault contract: `OUSDVault`, `OETHVault`, `OETHBaseVault` (Base), `OSVault` (Sonic) and `OETHPlumeVault` (Plume). `OETHPlumeVault` is being shut down and will be removed from the repo after all funds are withdrawn. -Vault logic is split across two implementation contracts: `VaultCore` (user-facing mint/redeem) and `VaultAdmin` (governance functions). -`VaultCore` inherits from `VaultAdmin` and is now deployed as a single implementation contract for simplicity. -Previously they were deployed as separate implementations with a shared proxy. This was because the contract was too big to deploy as a single implementation, but after the simplification, it can now be deployed as one. +Inheritance: `VaultStorage` → `VaultInitializer` → `VaultCore` (user-facing mint/redeem) → `VaultAdmin` (governance functions) → the concrete per-chain vault, deployed as a **single implementation** behind the proxy. (They were historically two implementations sharing one proxy because the combined contract exceeded the size limit; that split is gone.) ### Strategies (Yield Generation) Located in `contracts/strategies/`. Each strategy: @@ -155,35 +201,40 @@ scheduled action is added, removed, or its behaviour changes. ### Key Utility Files - `utils/addresses.js` - master address registry for all networks/contracts (~32KB) -- `utils/deploy.js` - deployment helper functions (use these patterns when writing deploy scripts) - `utils/constants.js` - protocol constants +- `utils/deploy.js` - legacy Hardhat deployment helpers (only relevant to the frozen `deploy/` scripts) ## Test Organization ``` -test/ - _fixture.js # Main fixture: deploys all contracts + mocks for unit tests - _fixture-base.js # Base network fixture - _fixture-sonic.js # Sonic network fixture - _hot-deploy.js # Hot deploy support for dev iteration - vault/ # Vault tests (unit + fork) - strategies/ # Strategy tests (unit + fork) - behaviour/ # Shared behavioral test suites (used across strategies) +tests/ # Foundry suite (canonical) + Base.t.sol # root test contract: actors, common setup + unit/ # mocked unit tests — aim for ~100% coverage here + fork/ # integration tests on a chain fork; deploy our contracts fresh, use real external protocols + smoke/ # live-deployment health checks; deploy nothing, use only what is on chain + invariant/ + mocks/ + utils/ # Addresses.sol, shared helpers +test/ # legacy Hardhat suite — frozen, not run in CI ``` -**Fork test files** are named `*.fork-test.js` and run against real deployed contracts on a network fork. +Layout mirrors `////{concrete,fuzz}/.t.sol`, with a per-contract `shared/Shared.t.sol` base. Tests interact with contracts **through interfaces** (`IVault`, `IOToken`, `IWOToken`, `IProxy`) and deploy implementations with `vm.deployCode` — never import a concrete contract into a test file; it drags the whole dependency tree into the test's compilation unit and destroys build caching. Full rules and gotchas: `tests/README.md`. -**Unit test files** are named `*.js` (without `.fork-test`) and run against local mocks. +## Deployment Scripts -**Behavior tests** (`test/behaviour/`) define reusable test suites (e.g., `shouldBehaveLikeStrategy`) that are composed into strategy-specific test files. +Foundry deploy scripts live in `scripts/deploy//` and are numbered. The file, contract, and constructor deployment ID must match: file `017_UpgradeVault.s.sol`, contract `$017_UpgradeVault`, constructor ID `"017_UpgradeVault"`. Start from `000_Example.s.sol` in the chain directory. Record every new address with `_recordDeployment("Name", addr)` and resolve prior entries with `resolver.resolve("Name")`. -**Fixtures**: Each test file imports from `_fixture.js` which uses `loadFixture()` for snapshot-based test isolation. The fixture deploys mocks and wires up contracts identically to mainnet structure. +`DeployManager.s.sol` discovers scripts, skips completed ones (state in `build/deployments-.json`), and simulates pending governance on forks. Mainnet governance proposals go through GovernorSix; Base and HyperEVM use their chain TimelockController; Sonic scripts apply fork-only governance effects in `_fork()`. -## Deployment Scripts +```bash +make simulate # fork simulation, no broadcast (NETWORK=base|hyperevm for other chains) +make deploy-mainnet # real deploy + Etherscan verify; also: deploy-base, deploy-hyperevm +make deploy-local # against a running pnpm node:anvil +``` -Located in `deploy/` and numbered sequentially (e.g., `001_ousd.js`, `002_vault.js`). Each script uses `hardhat-deploy` plugin conventions - exports a deploy function and tags. +For upgrades, call `_assertStorageSafe(type(X).name)` before `new X()` — see Storage Layout Checks below. After a real deploy the make target regenerates the Hardhat-format descriptors in `deployments//` (see the Talos section at the bottom). Framework internals: `scripts/deploy/README.md` and `scripts/deploy/ARCHITECTURE.md`. -When adding a new deployment script, increment the number and follow existing patterns in `utils/deploy.js` (especially `deployWithConfirmation` and `withConfirmation`). +Do not author new Hardhat deploy scripts in `deploy/`. ## Storage Layout Checks @@ -261,21 +312,20 @@ tests nothing. ## Roles & Access Control Four key roles used across all contracts: -- **Deployer** - deploys contracts (set via `DEPLOYER_PK` env var) -- **Governor** - timelock-controlled governance address (set via `GOVERNOR_PK` env var) +- **Deployer** - deploys contracts. Foundry deploys sign with the `deployerKey` keystore + `DEPLOYER_ADDRESS`; the legacy Hardhat CLI reads `DEPLOYER_PK` +- **Governor** - timelock-controlled governance address - **Strategist** - multisig for day-to-day operations - **Guardian** - emergency pause capability -For fork tests, these addresses are impersonated. Set `IMPERSONATE=0x...` env var to impersonate any account on a running fork node. +Foundry fork and smoke tests impersonate these with `vm.prank`. For Hardhat tasks against a running fork node, set `IMPERSONATE=0x...` to run as any account. ## Contract Verification -Use `yarn` (not `pnpm`) for verification. Always pass `--contract` flag to avoid slowdowns: +`make deploy-mainnet|base|hyperevm` verifies as part of the deploy (`--verify`; set the explorer API keys in `.env`). To check whether a local contract matches what is deployed: ```bash -yarn hardhat --network mainnet verify --contract contracts/vault/VaultAdmin.sol:VaultAdmin 0xADDRESS +make match file=contracts/vault/VaultCore.sol addr=0xADDRESS ``` - -Auto-verify on deploy: `VERIFY_CONTRACTS=true pnpm deploy:mainnet` +For manually verifying legacy Hardhat-deployed contracts, use `yarn hardhat verify` with an explicit `--contract` flag (see README.md). ## Logger Pattern @@ -287,12 +337,17 @@ log("something happened"); ## Foundry deploy files vs. Talos actions (MANDATORY CHECK) -The Talos ops automation (`contracts/tasks/actions/**` — harvest, rebases, `doAccounting`, validator ops, cross-chain relays, etc.) resolves the contracts it operates on from the hardhat-deploy artifacts in `contracts/deployments//.json` (addresses) and pinned entries in `contracts/utils/addresses.js` / action-local `*_BY_CHAIN_ID` maps. Foundry deploys write only `contracts/build/deployments-.json` (addresses, **no ABI**) and do **not** update `deployments/` or `addresses.js`. So redeploying a contract via Foundry can silently point a live Talos action at a stale address. +The Talos ops automation (`contracts/tasks/actions/**` — harvest, rebases, `doAccounting`, validator ops, cross-chain relays, etc.) resolves the contracts it operates on from the descriptors in `contracts/deployments//.json` (addresses) and pinned entries in `contracts/utils/addresses.js` / action-local `*_BY_CHAIN_ID` maps. Foundry's own broadcast state lives in `contracts/build/deployments-.json`. + +The `make deploy-mainnet|base|hyperevm` targets close most of this gap automatically: after the broadcast they run `scripts/create-hardhat-format-descriptors.js`, which rewrites `deployments//.json` for whatever was just deployed (even when verification fails; `make deploy-local` deliberately skips it). What is **not** automatic — and can silently point a live Talos action at a stale address or ABI: +- a raw `forge script` run outside the make targets never updates descriptors; +- pinned addresses in `utils/addresses.js` and action-local `*_BY_CHAIN_ID` maps; +- the curated `abi/.json` files when a callable interface changes. **Whenever you create or modify a Foundry deploy script (`contracts/scripts/deploy/**/*.s.sol`), you MUST:** 1. List every contract the deploy script deploys or upgrades (proxy or implementation). 2. Grep `contracts/tasks/actions/**` and the utils they import for those contract/deployment names and any pinned addresses (`utils/addresses.js`, action-local `*_BY_CHAIN_ID` maps, `abi/*.json` call surfaces). -3. For any overlap, update the corresponding `deployments//.json` address (and the curated `abi/.json` if the callable interface changed) or the pinned address in the action/`addresses.js`, and call it out explicitly in the PR description. +3. For any overlap, confirm the descriptor refresh covers it, update pinned addresses and the curated `abi/.json` if the callable interface changed, and call it out explicitly in the PR description. 4. If you cannot verify whether an action is affected, say so explicitly in the PR — never assume "no impact". Talos action ABIs come from curated interface ABIs in `contracts/abi/*.json` or inline human-readable ABIs — never from `deployments/*.json` `.abi` (proxy artifacts are admin-only and concrete artifacts can be stale) and never from `artifacts/` (not shipped in the actions image). Addresses are the deployed truth (`deployments/*.json` `.address` / pinned).