Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
345566f
support aerodrome
junta Jul 1, 2026
dd8eb91
Merge branch 'main' into feat/suppor-aerodrome
junta Jul 3, 2026
1318d7b
Refactor RatehopperAerodromePositions contract and update deployment …
junta Jul 3, 2026
f3a1a2c
Add reviveIgnitionValues function to handle bigint serialization in c…
junta Jul 3, 2026
cfc531b
introduce handler pattern
junta Jul 29, 2026
fe1a9ce
Rename debt-side types to DebtProtocol namespace and apply review fixes
junta Jul 30, 2026
799d629
Harden yield handlers and Aerodrome integration
junta Aug 3, 2026
ba6d596
Remove legacy RatehopperAerodromePositions and consolidate contract f…
junta Aug 3, 2026
1294dc6
Remove dead code and simplify yield handler hierarchy
junta Aug 3, 2026
1bf19ec
Add Uniswap V3 fork test and organize test/ into area folders
junta Aug 3, 2026
54c1598
Organize contracts/ into area folders
junta Aug 3, 2026
99e3b0c
Retire the standalone UniV3 helper deploy and renumber yield deploy to 2
junta Aug 3, 2026
2302614
Record uint8-id yield stack deployment addresses
junta Aug 3, 2026
0fd70e3
Drop duplicate post-repay dust refund and stabilize leveraged-positio…
junta Aug 3, 2026
be874f5
Restore 2-day timelock default and close yield branch-coverage gaps
junta Aug 5, 2026
b1dc8ce
Stabilize debt integration tests
junta Aug 5, 2026
95b8ef8
Complete yield statement/function coverage and fix fork-test bigint t…
junta Aug 5, 2026
2467c69
Add SafeYieldManager LP switching
junta Aug 5, 2026
d576d26
Support arbitrary token pairs in the yield stack
junta Aug 6, 2026
eaf65ec
Harden approvals and collect LP fees for staked positions
junta Aug 7, 2026
76aaca8
Deduplicate yield handler flows, test helpers, and ops scripts
junta Aug 10, 2026
212e09e
Claim-only staked harvest with optional reward->USDC swap
junta Aug 10, 2026
25c4512
Add Uniswap V4 yield handler and harden checks
junta Aug 10, 2026
6609951
Update ratehopperUniV3Positions.ts
junta Aug 10, 2026
d1a8046
Validate timelock wiring, pin stake pools, and harden ERC20 return ch…
junta Aug 10, 2026
0c15fb6
Cover V4 switchLp pairs and run yield fork tests against a real Safe
junta Aug 13, 2026
485fd6d
Fix flaky branch-coverage gate: deterministic USDC-token0 test, pinne…
junta Aug 13, 2026
baab02f
Import Uniswap V4 math from exact-pinned npm packages
junta Aug 13, 2026
61c2d9e
Extract shared ERC20 optional-return check into TokenReturnLib
junta Aug 13, 2026
430e868
Fold UniV4 handler deploy into the DeployYieldManager module
junta Aug 13, 2026
a73e2d2
Gate openLp and switchLp pool tokens on the registry whitelist
junta Aug 13, 2026
89cd772
Add collectLpBySafe ops script and env-driven Safe/deployment config
junta Aug 13, 2026
7ed4b6a
Bring docs in line with the V4-era yield stack before PR
junta Aug 13, 2026
3f4ca64
Cover the ERC20-currency0 V4 path in the fork suite
junta Aug 14, 2026
670dd35
Extend yield ops scripts for staking, reward swaps, and pool allow-li…
junta Aug 14, 2026
06d0390
Cover the USDC-as-token0 Aerodrome pair in the fork suite
junta Aug 14, 2026
2a79459
Prove the fee flows against real accrued fees in the fork suite
junta Aug 14, 2026
b26395e
Rework switchLp to move positions in kind - no swaps, no swap slippage
junta Aug 18, 2026
80e36f3
Fix CI: prettier formatting and branch coverage on the in-kind switch
junta Aug 18, 2026
3863cc9
Share one mint path between openLp and openLpInKind in both handlers
junta Aug 19, 2026
8b39e10
L-01: keep a partially closed position staked in its pinned gauge
junta Aug 19, 2026
7c21783
M-03: charge feeCollectBps on every gauge emission claim
junta Aug 19, 2026
2d7c74e
L-02: build script minima from exact integer math instead of zero
junta Aug 19, 2026
c79a7a0
I-01/I-02/I-03: refund declined repayments, document the custody model
junta Aug 19, 2026
52a8aad
H-01: floor every router call at a reference TWAP the caller cannot s…
junta Aug 20, 2026
720ad2a
M-01/M-02: value the residue an in-kind switch cannot redeploy
junta Aug 20, 2026
e387d43
Pre-PR review fixes: a broken deploy call, a dead error, an untrue co…
junta Aug 20, 2026
471c72f
Complete yield security remediation coverage
junta Aug 21, 2026
ef3a4e3
Restore the branch-coverage gate over the new allow-list bookkeeping
junta Aug 21, 2026
e549c64
Seed TWAP references at construction so a fresh deploy can trade imme…
junta Aug 21, 2026
bacb086
Pin the constructor's config invariants with tests instead of comments
junta Aug 21, 2026
1a06256
Fix CI: the seeded constructor overran the stack under coverage
junta Aug 21, 2026
bf502f6
Sync deployment addresses from the active --deployment-id, not chain-…
junta Aug 24, 2026
b3cd0f9
Sync exported ABI and deployment manifest after the yield-v2 rotation
junta Aug 24, 2026
6460beb
Fund debt-test fixtures on the fork instead of the live test wallet
junta Aug 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 63 additions & 30 deletions .env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@ EXPLORER_KEY=
# endpoint; a dedicated archive node is strongly recommended for reliable forking.
BASE_RPC_URL=

# [OPTIONAL] Pin the global Hardhat Base fork. Tests default to 49470000 for
# deterministic protocol state; override this only when intentionally testing
# newer Base state.
# Leave empty when running ParaSwap integration tests because its API quotes
# against the latest chain state. Set a block only for fully deterministic tests.
BASE_FORK_BLOCK_NUMBER=

# =============================================================================
# ACCESS CONTROL ADDRESSES
# =============================================================================
Expand All @@ -26,59 +33,85 @@ BASE_RPC_URL=
ADMIN_ADDRESS=

# [REQUIRED] Operator address that can call executeDebtSwap() and other SafeDebtManager/LeveragedPosition functions
# This address is stored in ProtocolRegistry and can only be changed via TimelockController (8-hour delay by default)
# This address is stored in ProtocolRegistry and can only be changed via TimelockController (2-day delay by default)
SAFE_OPERATOR_ADDRESS=

# [REQUIRED] Pauser address that can pause/unpause SafeDebtManager and LeveragedPosition in emergencies
# Can be the same as ADMIN_ADDRESS, or a separate EOA for faster emergency response
PAUSER_ADDRESS=

# =============================================================================
# TIMELOCK DEPLOYMENT (shared by deploy:1_core and deploy:2_univ3_helper)
# TIMELOCK DEPLOYMENT (shared by deploy:1_core and deploy:2_yield_manager)
# =============================================================================

# [OPTIONAL] EOA/multisig granted BOTH proposer and executor roles on the deployed
# TimelockController. Falls back to ADMIN_ADDRESS if unset.
TIMELOCK_ADMIN=

# [OPTIONAL] Minimum delay (seconds) before queued timelock ops can execute.
# Defaults to 28800 (8 hours).
# Defaults to 172800 (2 days).
TIMELOCK_DELAY=

# =============================================================================
# UNISWAP V3 HELPER DEPLOYMENT (deploy:2_univ3_helper)
# YIELD DEPLOY CONFIG — for deploy:2_yield_manager.
#
# Resolution order: SYM_* module override → shared unprefixed name below →
# legacy RHP_* fallback (addresses only) → default. An EMPTY value (X=)
# counts as unset and falls through, so leave optional variables commented
# out unless you mean to override them.
# =============================================================================

# [REQUIRED] Treasury address that collects performance + collect fees.
# The deploy reverts without a valid treasury.
RHP_TREASURY=
# The deploys revert without a valid treasury.
TREASURY=

# [OPTIONAL] ProtocolRegistry to wire RHP to. If unset, falls back to
# [OPTIONAL] ProtocolRegistry to wire against. Falls back to
# PROTOCOL_REGISTRY_ADDRESS in contractAddresses.ts (auto-synced by registry/core deploys).
RHP_REGISTRY=
# REGISTRY=

# [OPTIONAL] DEFAULT_ADMIN_ROLE holder on RatehopperUniV3Positions. Falls back to ADMIN_ADDRESS.
RHP_INITIAL_ADMIN=
# [OPTIONAL] DEFAULT_ADMIN_ROLE holder. Falls back to ADMIN_ADDRESS.
# INITIAL_ADMIN=

# [OPTIONAL] Reuse an existing TimelockController instead of deploying a new one.
# When set, the shared TimelockControllerModule is skipped for this deploy.
# When set, the shared TimelockControllerModule is skipped. Read by
# deploy:2_yield_manager only (module override: SYM_TIMELOCK); the registry
# deploy uses REGISTRY_TIMELOCK below.
RHP_TIMELOCK=

# [OPTIONAL] Performance fee on net profit at closeLp (bps). Default 1000 (10%).
RHP_PERFORMANCE_FEE_BPS=

# [OPTIONAL] Fee on harvested LP fees via collectLp/closeLp (bps). Default 250 (2.5%).
RHP_FEE_COLLECT_BPS=

# [OPTIONAL] Hard upper bound on BOTH fees (bps). Default 2000 (20%).
RHP_MAX_FEE_BPS=

# [OPTIONAL] Floor on NPM mint liquidity (dust-close hardening). Default 10000. Set 0 to disable.
RHP_MIN_POSITION_LIQUIDITY=

# [OPTIONAL] Floor on pool.liquidity() for spot-price reads (manipulation hardening).
# Default 0 (disabled). See 2_DeployUniV3Helper.ts docs / RUNBOOK before raising.
RHP_MIN_POOL_LIQUIDITY=
# [OPTIONAL] deploy:0_registry only — DEFAULT_ADMIN_ROLE holder on the new
# ProtocolRegistry (falls back to ADMIN_ADDRESS) and an existing
# TimelockController to reuse instead of the shared sub-module.
# REGISTRY_INITIAL_ADMIN=
# REGISTRY_TIMELOCK=

# [OPTIONAL] Fee config (bps). Defaults: 1000 (10%) / 250 (2.5%) / 2000 (20%).
# PERFORMANCE_FEE_BPS=1000
# FEE_COLLECT_BPS=250
# MAX_FEE_BPS=2000

# [OPTIONAL] Floor on NPM mint liquidity (dust-close hardening) and on
# pool.liquidity() for spot-price reads (manipulation hardening).
# Defaults: 10000 / 0 (disabled). See the module docs in ignition/modules/ before raising.
# MIN_POSITION_LIQUIDITY=10000
# MIN_POOL_LIQUIDITY=0

# SYM_* overrides (rarely needed): same suffix with the SYM_ prefix, e.g.
# SYM_TREASURY / SYM_MAX_FEE_BPS. The pauser on SafeYieldManager comes from
# SYM_PAUSER / PAUSER_ADDRESS / ADMIN_ADDRESS.
# SafeYieldManager additionally supports per-protocol floors:
# SYM_UNIV3_MIN_POSITION_LIQUIDITY=10000
# SYM_AERODROME_MIN_POSITION_LIQUIDITY=10000
# SYM_UNIV4_MIN_POSITION_LIQUIDITY=10000
# SYM_UNIV3_MIN_POOL_LIQUIDITY=0
# SYM_AERODROME_MIN_POOL_LIQUIDITY=0
# SYM_UNIV4_MIN_POOL_LIQUIDITY=0

# [OPTIONAL] Uniswap V4 address overrides — default to the canonical Base
# addresses in contractAddresses.ts; only set when targeting another network.
# SYM_UNIV4_POSITION_MANAGER=
# SYM_UNIVERSAL_ROUTER=
# SYM_PERMIT2=
# SYM_UNIV4_STATE_VIEW=

# =============================================================================
# TESTING CONFIGURATION (for running tests and scripts)
Expand All @@ -92,9 +125,6 @@ TESTING_SAFE_OWNER_KEY=
# [REQUIRED for Safe tests] Safe wallet address for integration tests
TESTING_SAFE_WALLET_ADDRESS=

# [OPTIONAL] Address of the Safe wallet owner (used in deployRolesProxy.ts as Roles module owner)
TESTING_SAFE_OWNER_ADDRESS=

# =============================================================================
# TIMELOCK OPERATIONS (for post-deployment configuration changes)
# =============================================================================
Expand All @@ -105,9 +135,12 @@ TIMELOCK_ADDRESS=
# [REQUIRED] ProtocolRegistry contract address (set after deployment)
PROTOCOL_REGISTRY_ADDRESS=

# [REQUIRED] New Paraswap V6 address (for timelock-update-paraswap.ts script)
# [REQUIRED] New Paraswap V6 address (for timelockUpdateParaswap.ts script)
NEW_PARASWAP_ADDRESS=

# [REQUIRED] New operator address (for timelockUpdateOperator.ts script)
NEW_OPERATOR_ADDRESS=

# [OPTIONAL] Set to "true" to execute a scheduled timelock operation (default: schedule only)
EXECUTE=

Expand Down
22 changes: 15 additions & 7 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,19 +33,27 @@ jobs:
- name: Compile
run: yarn compile

# Coverage is scoped to the RatehopperUniV3Positions suite. The legacy
# debt-swap / protocol-handler suites are Base-fork tests that are not
# yet fully covered; running them here would fail the step before the
# gate. Expand the --testfiles glob (in package.json `coverage:rhp`) as
# coverage for those contracts is brought up.
# Coverage is scoped to the RatehopperUniV3Positions and
# SafeYieldManager suites. The debt-swap / protocol-handler suites are
# Base-fork tests that are not yet fully covered; running them here
# would fail the step before the gate. Expand the --testfiles glob (in
# package.json `coverage:gated`) as coverage for those contracts is
# brought up.
#
# BASE_RPC_URL is recommended: an archive RPC secret gives reliable
# forking. Without it the config falls back to the public
# https://mainnet.base.org endpoint.
- name: Run coverage (RatehopperUniV3Positions)
#
# BASE_FORK_BLOCK_NUMBER pins the default-network fork so the run is
# deterministic: without it the fork tracks the latest Base block and
# the well-known Hardhat deployer accounts inherit whatever on-chain
# nonce they have there, shifting every mock deploy address (and the
# branch coverage that depends on address ordering) from run to run.
- name: Run coverage (gated contracts)
env:
BASE_RPC_URL: ${{ secrets.BASE_RPC_URL }}
run: yarn coverage:rhp
BASE_FORK_BLOCK_NUMBER: "49470000"
run: yarn coverage:gated

- name: Enforce branch-coverage gate
run: yarn coverage:check
8 changes: 7 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,13 @@ node_modules

# Hardhat Ignition default folder for deployments against a local node
ignition/deployments/chain-*
ignition/deployments/local-operator-v1/
ignition/deployments/yield-v2/

# Local deployment records (keep the stable Base manifest reviewable)
/deployments/*
!/deployments/base.json
/.vscode
yarn-error.log
eth-sdk/abis/*
.claude
.claude
9 changes: 6 additions & 3 deletions .solcover.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
module.exports = {
// Mocks are test-only scaffolding — exclude from instrumentation/report.
skipFiles: ["mocks/"],
// RatehopperUniV3Positions compiles with viaIR; let coverage configure the
// Yul optimizer so instrumentation doesn't trip "stack too deep".
configureYulOptimizer: true,
// RatehopperUniV3Positions compiles with viaIR; instrumented branches in
// large frames (e.g. switchLp) trip "stack too deep" unless the Yul
// optimizer pipeline is reduced to the minimum (Foundry ir-minimum
// equivalent). Supersedes configureYulOptimizer, which stopped being
// enough once switchLp grew.
irMinimum: true,
mocha: {
parallel: false,
},
Expand Down
21 changes: 15 additions & 6 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,24 @@ RateHopper Contracts is a DeFi smart contract system enabling automated debt pos
### Core Contracts

- **SafeDebtManager.sol**: Main entry point for debt swaps via Gnosis Safe
- **SafeYieldManager.sol**: Single Safe-module entry point for yield (LP) protocols; delegatecalls stateless yield handlers, shared state in ERC-7201 namespace (`YieldStorage`)
- **LeveragedPosition.sol**: Creates leveraged positions across protocols
- **ProtocolRegistry.sol**: Central registry for token mappings, operator, and protocol configs
- **Types.sol**: Shared type definitions
- **Types.sol**: Shared type definitions (`DebtProtocol` enum, `YIELD_PROTOCOL_*` uint8 id constants)
- **RatehopperUniV3Positions.sol**: Legacy standalone yield module, deployed and serving existing positions; superseded by SafeYieldManager for new positions (coexistence — do not modify)

### Protocol Handlers (`contracts/protocols/`, `contracts/protocolsSafe/`)
### Protocol Handlers (`contracts/debt/handlers/`)

- **AaveV3Handler.sol**, **CompoundHandler.sol**, **MoonwellHandler.sol**, **FluidSafeHandler.sol**
- **AaveV3DebtHandler.sol**, **CompoundDebtHandler.sol**, **MorphoDebtHandler.sol**, **MoonwellDebtHandler.sol**, **FluidSafeDebtHandler.sol** extend **BaseDebtHandler.sol**
- Each implements: `getDebtAmount`, `switchIn`, `switchFrom`, `switchTo`, `repay`

### Yield Handlers (`contracts/yield/handlers/`)

- **BaseYieldHandler.sol** owns the shared LP flow, protocol diffs in virtual hooks; **V3StyleYieldHandler.sol** implements the hooks against canonical Uniswap V3 interfaces (protocol id as constructor arg); **UniV3YieldHandler.sol** extends V3StyleYieldHandler, **AerodromeYieldHandler.sol** extends BaseYieldHandler directly
- **UniV4YieldHandler.sol** implements `IYieldHandler` directly (V4 singleton/actions model doesn't fit the V3-shaped hooks): PoolKey pool params (`keccak256(poolParam)` == V4 PoolId), Permit2 two-step approvals, UniversalRouter swaps, native ETH pools supported; V4 math imported from exact-pinned npm packages (`@uniswap/v4-core@1.0.2`, `@uniswap/v4-periphery@1.0.3` — keep exact versions, no `^`); V4 interfaces remain hand-written minimal versions vendored under `contracts/interfaces/uniswapV4/` (do not replace with official interfaces — their `Currency`/`PositionInfo` types would leak into handler code)
- Stateless delegatecall targets: MUST NOT declare storage variables; mutable state only via `YieldStorage._yieldStorage()`
- Pool selection params are ABI-encoded bytes (`uint24` feeTier / `int24` tickSpacing / full V4 `PoolKey` tuple)

### Access Control

- **DEFAULT_ADMIN_ROLE**: Routine operations (whitelist, token mappings)
Expand All @@ -42,14 +51,14 @@ RateHopper Contracts is a DeFi smart contract system enabling automated debt pos

### Naming

- Handlers: `<Protocol>Handler.sol`
- Handlers: `<Protocol>DebtHandler.sol` (debt) / `<Protocol>YieldHandler.sol` (yield)
- Interfaces: `I<ContractName>.sol`
- Tests: `test/<feature>.ts`
- Tests: `test/<area>/<feature>.ts` — areas: `debt/`, `registry/`, `yield/`, `legacy/` (deployed standalone modules), `helpers/` (fixtures/utils, no tests)

## Key Files

- `contractAddresses.ts`: Token and protocol addresses
- `test/constants.ts`, `test/utils.ts`, `test/deployUtils.ts`: Test helpers
- `test/helpers/constants.ts`, `test/helpers/utils.ts`, `test/helpers/deployUtils.ts`: Test helpers

## Security Requirements

Expand Down
Loading
Loading