Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
e0d3f04
Add Uniswap V3 addresses and update configurations
junta May 22, 2026
0008cfb
Refactor RateHopperPositions to use ProtocolRegistry instead of SafeD…
junta May 22, 2026
8b2b248
Add performance fee management to RateHopperPositions
junta May 22, 2026
e332e42
Update rateHopperPositions.ts
junta May 22, 2026
4a1a35e
Enhance _collectLp function in RateHopperPositions to include current…
junta May 22, 2026
04c6457
Enhance RateHopperPositions and deployment scripts
junta May 25, 2026
25bfadf
Refactor RateHopper to RatehopperUniV3 and update deployment scripts
junta May 26, 2026
6b985a3
Refactor RatehopperUniV3Positions to use AccessControl and introduce …
junta May 27, 2026
bc230bd
Enhance README and deployment module for RatehopperUniV3Positions
junta May 27, 2026
b6d688c
Update RatehopperUniV3Positions contract and tests for swap amount va…
junta May 27, 2026
5f736f9
Refactor RatehopperUniV3Positions contract and enhance tests for NFT …
junta May 27, 2026
24ab3a8
Update RatehopperUniV3Positions contract documentation and tests for …
junta May 27, 2026
e4cc229
Implement timelock access control in LeveragedPosition, RatehopperUni…
junta May 29, 2026
7a71877
Refactor test imports to streamline safeAddress usage
junta May 29, 2026
33e7f45
Refactor deployment modules to utilize shared TimelockControllerModule
junta May 29, 2026
0e03c84
Add minPositionLiquidity feature and related validations in Ratehoppe…
junta Jun 5, 2026
3db6ec6
update to add more tests in
tomafc330 Jun 8, 2026
bdd9b48
update to sollint all files, updates to the deployment script to better
tomafc330 Jun 11, 2026
338f891
fix the build
tomafc330 Jun 11, 2026
d8c3b09
Add timelockUpdateOperator script for updating operator address
junta Jun 13, 2026
26d1fec
Update README.md with detailed instructions for timelock operations
junta Jun 13, 2026
17bd72a
Update README.md
junta Jun 13, 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
92 changes: 74 additions & 18 deletions .env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -2,68 +2,124 @@
# DEPLOYMENT CONFIGURATION
# =============================================================================

# Private key for deployment to production networks (without 0x prefix)
# [REQUIRED] Private key for deployment to production networks (without 0x prefix)
# SECURITY: Never commit this file with actual values. Use a hardware wallet or secure key management.
DEPLOYER_PRIVATE_KEY=

# Block explorer API key for contract verification, needs Etherscan.io! Sign up at: https://etherscan.io/apidashboard
# [REQUIRED for --verify] Block explorer API key for contract verification, needs Etherscan.io! Sign up at: https://etherscan.io/apidashboard
EXPLORER_KEY=

# [OPTIONAL] Base mainnet RPC URL. Used for deployments to the `base` network and
# for fork-based tests/coverage. Falls back to the public https://mainnet.base.org
# endpoint; a dedicated archive node is strongly recommended for reliable forking.
BASE_RPC_URL=

# =============================================================================
# ACCESS CONTROL ADDRESSES
# =============================================================================

# Admin address - receives ownership of deployed contracts and acts as:
# [REQUIRED] Admin address - receives ownership of deployed contracts and acts as:
# - TimelockController proposer and executor
# - ProtocolRegistry DEFAULT_ADMIN_ROLE holder
# - Final owner of SafeDebtManager and LeveragedPosition after deployment
# IMPORTANT: Use a multisig (e.g., Gnosis Safe) for production deployments
ADMIN_ADDRESS=

# Operator address that can call executeDebtSwap() and other SafeDebtManager/LeveragedPosition functions
# [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 (2-day delay)
SAFE_OPERATOR_ADDRESS=

# Pauser address that can pause/unpause SafeDebtManager and LeveragedPosition in emergencies
# [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)
# =============================================================================

# [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 172800 (2 days). Lower on testnets only.
TIMELOCK_DELAY=

# =============================================================================
# UNISWAP V3 HELPER DEPLOYMENT (deploy:2_univ3_helper)
# =============================================================================

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

# [OPTIONAL] ProtocolRegistry to wire RHP to. If unset, falls back to
# PROTOCOL_REGISTRY_ADDRESS in contractAddresses.ts (auto-synced by deploy:1_core).
RHP_REGISTRY=

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

# [OPTIONAL] Reuse an existing TimelockController instead of deploying a new one.
# When set, the shared TimelockControllerModule is skipped for this deploy.
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=

# =============================================================================
# TESTING CONFIGURATION (for running tests and scripts)
# =============================================================================

# Private key for a Safe wallet owner (used for signing transactions, enabling modules, etc.)
# This should be a key that owns or is a signer on the test Safe wallet
# [REQUIRED for Safe tests] Private key for a Safe wallet owner (used for signing transactions, enabling modules, etc.)
# This should be a key that owns or is a signer on the test Safe wallet.
# Safe integration tests are skipped if this (or TESTING_SAFE_WALLET_ADDRESS) is unset.
TESTING_SAFE_OWNER_KEY=

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

# Safe wallet address for integration tests
# [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)
# =============================================================================

# TimelockController contract address (set after deployment)
# [REQUIRED] TimelockController contract address (set after deployment)
TIMELOCK_ADDRESS=

# ProtocolRegistry contract address (set after deployment)
# [REQUIRED] ProtocolRegistry contract address (set after deployment)
PROTOCOL_REGISTRY_ADDRESS=

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

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

# Unique identifier for timelock operations (optional, auto-generated if not set)
# [OPTIONAL] Unique identifier for timelock operations (auto-generated if not set)
OPERATION_ID=

# =============================================================================
# OPTIONAL / DEVELOPMENT
# =============================================================================

# Enable gas reporting in tests (set to any value to enable)
# REPORT_GAS=true
# [OPTIONAL] Enable gas reporting in tests (set to any value to enable)
# REPORT_GAS=true

# [OPTIONAL] Override the chain id used by scripts/syncRegistryAddress.js. Default 8453 (Base).
# CHAIN_ID=8453
51 changes: 51 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
name: CI

on:
push:
branches: [main]
pull_request:

jobs:
lint-test-coverage:
name: Lint & branch-coverage gate
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: "22"

- name: Enable Corepack (Yarn 4)
run: corepack enable

- name: Install dependencies
run: yarn install --immutable

- name: Lint Solidity (solhint)
run: yarn lint:sol

- name: Check formatting (prettier)
run: yarn format:check

- 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.
#
# 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)
env:
BASE_RPC_URL: ${{ secrets.BASE_RPC_URL }}
run: yarn coverage:rhp

- name: Enforce branch-coverage gate
run: yarn coverage:check
1 change: 1 addition & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"plugins": ["prettier-plugin-solidity"],
"printWidth": 120,
"singleQuote": false,
"tabWidth": 4,
Expand Down
5 changes: 5 additions & 0 deletions .solcover.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
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,
mocha: {
parallel: false,
},
Expand Down
3 changes: 1 addition & 2 deletions .solhint.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
{
"extends": "solhint:recommended",
"plugins": ["prettier"],
"rules": {
"prettier/prettier": "error",
"no-console": "off",
"avoid-low-level-calls": "off",
"func-visibility": "off",
"custom-errors": "off"
}
}
4 changes: 4 additions & 0 deletions .solhintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Vendored / external code — third-party libraries and protocol ABIs that we
# do not control. Lint only first-party contracts.
contracts/dependencies/
contracts/interfaces/
100 changes: 89 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ RateHopper Contracts is a smart contract system that enables users to automatica

- **Leveraged Positions**: Enables creation of leveraged positions across supported protocols.

- **Uniswap V3 LP Lifecycle**: `RatehopperUniV3Positions` is a Gnosis Safe module that opens, harvests fees from, and closes WETH/USDC LP positions atomically. Charges a configurable performance fee on profit at close, plus a separate fee on accrued LP fees. Critical setters are timelock-gated.

## Architecture

The system consists of several key components:
Expand Down Expand Up @@ -56,7 +58,15 @@ The system consists of several key components:

5. **LeveragedPosition.sol**: Facilitates creation of leveraged positions across protocols.

6. **Morpho Libraries**: Supporting libraries for the Morpho protocol:
6. **RatehopperUniV3Positions.sol**: Standalone Gnosis Safe module for Uniswap V3 WETH/USDC LP lifecycle. Three external entry points:

- `openLp()` — splits the Safe's USDC, swaps half to WETH via the pinned SwapRouter02, mints a Uniswap V3 LP NFT on the Safe.
- `closeLp()` — partial or full unwind (`exitBps`): harvests accrued fees, `decreaseLiquidity`, collects principal, optionally `burn`s, swaps the WETH leg back to USDC.
- `collectLp()` — mid-position fee harvest with no decrease/burn.

Caller passes per-call `swapAmountOutMin` and `deadline` (audit fixes C-01 / H-02). The constructor rejects any non-WETH/USDC token pair (M-08). Performance fee is charged on net profit (`currentValueUsd6 - basisUsd6`); fee-collect is charged on accrued fees only. All fee setters are gated by `CRITICAL_ROLE` on a TimelockController (H-04); `rescueToken` and similar emergency ops are gated by `DEFAULT_ADMIN_ROLE`.

7. **Morpho Libraries**: Supporting libraries for the Morpho protocol:

- `MathLib.sol`: Provides fixed-point arithmetic operations for the Morpho protocol
- `SharesMathLib.sol`: Handles share-to-asset conversion with virtual shares to protect against share price manipulations
Expand Down Expand Up @@ -165,11 +175,25 @@ struct ParaswapParams {
Create a `.env` file with the following required variables (use `.env.sample` as a template):

```env
ADMIN_ADDRESS=0x... # Initial admin and timelock proposer/executor
# Core deploy
ADMIN_ADDRESS=0x... # Initial admin and timelock proposer/executor (used by 1_DeployCore AND 2_DeployUniV3Helper)
SAFE_OPERATOR_ADDRESS=0x... # Operator address for Safe interactions
PAUSER_ADDRESS=0x... # Address that can pause contracts
DEPLOYER_PRIVATE_KEY=... # Private key for deployment
EXPLORER_KEY=... # Block explorer API key for verification

# Optional — RatehopperUniV3Positions deploy (yarn deploy:2_univ3_helper)
RHP_REGISTRY=0x... # ProtocolRegistry address. Falls back to PROTOCOL_REGISTRY_ADDRESS in contractAddresses.ts
RHP_TREASURY=0x... # Treasury that collects feeCollectBps + performanceFeeBps cuts. REQUIRED.
RHP_INITIAL_ADMIN=0x... # DEFAULT_ADMIN_ROLE holder (rescueToken etc.). Falls back to ADMIN_ADDRESS
RHP_TIMELOCK=0x... # Reuse an existing TimelockController instead of deploying a new one
RHP_PERFORMANCE_FEE_BPS=1000 # Performance fee on profit at closeLp (bps). Default 1000 (10%)
RHP_FEE_COLLECT_BPS=250 # Fee on accrued LP fees harvested via collectLp/closeLp (bps). Default 250 (2.5%)
RHP_MAX_FEE_BPS=2000 # Hard upper bound on BOTH fees (bps). Default 2000 (20%)

# Optional — TimelockController inside 2_DeployUniV3Helper
TIMELOCK_ADMIN=0x... # Proposer + executor on the new timelock. Falls back to ADMIN_ADDRESS
TIMELOCK_DELAY=172800 # Min delay before queued ops execute (seconds). Default 172800 (2 days)
```
## Setup and Development

Expand Down Expand Up @@ -230,16 +254,16 @@ Hardhat Ignition is a declarative deployment framework. Instead of writing imper
- **Supports `--verify`** to automatically submit contracts to Etherscan after deployment
- **Supports `--reset`** to wipe previous state and redeploy from scratch

All contracts are defined in a single module at `ignition/modules/DeployAll.ts`. Every step is chained sequentially via `after` dependencies to avoid nonce race conditions.
The core contracts are defined in a single module at `ignition/modules/1_DeployCore.ts`. Every step is chained sequentially via `after` dependencies to avoid nonce race conditions.

### Deploy All Contracts

Deploy everything in one command:

```bash
yarn deploy
yarn deploy:1_core
# or equivalently:
npx hardhat ignition deploy ignition/modules/DeployAll.ts --network base --verify --reset
npx hardhat ignition deploy ignition/modules/1_DeployCore.ts --network base --verify --reset
```

This deploys all contracts sequentially in a single transaction chain:
Expand All @@ -253,6 +277,23 @@ This deploys all contracts sequentially in a single transaction chain:
7. **LeveragedPosition** → `transferOwnership` to `ADMIN_ADDRESS`
8. **SafeExecTransactionWrapper**

### Deploy RatehopperUniV3Positions (Standalone)

`RatehopperUniV3Positions` is deployed separately because it sits on top of an existing `ProtocolRegistry` and needs its own TimelockController for fund-impacting setters.

```bash
yarn deploy:2_univ3_helper
# or equivalently:
npx hardhat ignition deploy ignition/modules/2_DeployUniV3Helper.ts --network base --verify
```

This module by default deploys:

1. **TimelockController** (proposer + executor = `TIMELOCK_ADMIN` ?? `ADMIN_ADDRESS`; delay = `TIMELOCK_DELAY` ?? 2 days)
2. **RatehopperUniV3Positions** (wired to the registry from `RHP_REGISTRY` ?? `PROTOCOL_REGISTRY_ADDRESS`; CRITICAL_ROLE granted to the timelock from step 1)

To reuse an existing TimelockController instead of deploying a new one, set `RHP_TIMELOCK=0x...` — the module skips step 1 and points RHP at the supplied address. See `## Environment Variables` for the full list of optional knobs.

### Deployment Output

After deployment, Ignition saves state to:
Expand All @@ -272,7 +313,7 @@ cat ignition/deployments/chain-8453/deployed_addresses.json

### Contract Verification

The `--verify` flag on `yarn deploy` may fail due to a known `hardhat-verify` v2.x bug with Etherscan's V2 API (the plugin's GET requests strip the `chainid` parameter). Use the standalone verification script instead:
The `--verify` flag on `yarn deploy:1_core` may fail due to a known `hardhat-verify` v2.x bug with Etherscan's V2 API (the plugin's GET requests strip the `chainid` parameter). Use the standalone verification script instead:

```bash
yarn verify
Expand All @@ -290,19 +331,56 @@ This script:

### Timelock Operations

For critical operations (updating Paraswap address):
Critical `ProtocolRegistry` setters (`setParaswapV6`, `setOperator`) carry `CRITICAL_ROLE` and revert unless `msg.sender` is the timelock, so they must be scheduled and executed through the `TimelockController` (2-day delay). Each script is a two-step flow: schedule, wait for the delay, then re-run with `EXECUTE=true` reusing the same `OPERATION_ID` printed during scheduling.

#### Finding the TimelockController address

`SafeDebtManager` does not store the timelock itself — it only keeps an immutable `registry` reference, and the `TimelockController` address lives on the `ProtocolRegistry` (`timelock()` getter). So given a deployed `SafeDebtManager`, resolve the timelock by hopping through the registry.

**On-chain (authoritative — this is the address the contracts actually enforce):**

```bash
# 1. Read the registry from the deployed SafeDebtManager
REGISTRY=$(cast call <SAFE_DEBT_MANAGER_ADDRESS> "registry()(address)" --rpc-url <BASE_RPC_URL>)

# 2. Read the timelock from that registry
cast call $REGISTRY "timelock()(address)" --rpc-url <BASE_RPC_URL>
```

`SafeDebtManager.registry()` and `ProtocolRegistry.timelock()` are both public getters, so any RPC reader (cast, ethers, a block explorer's "Read Contract" tab) works.

**Off-chain (from this repo's Ignition deployment):** the address is recorded under the `TimelockControllerModule#TimelockController` key — the timelock is a shared sub-module, so it keeps that stable ID regardless of which top-level module deployed it.

```bash
cat ignition/deployments/chain-8453/deployed_addresses.json
# → look for "TimelockControllerModule#TimelockController"
```

Use the resulting address as `TIMELOCK_ADDRESS` in the commands below.

**Updating the Paraswap address:**

```bash
# Schedule operation (requires proposer role)
TIMELOCK_ADDRESS=0x... PROTOCOL_REGISTRY_ADDRESS=0x... NEW_PARASWAP_ADDRESS=0x... \
yarn hardhat run scripts/timelock-update-paraswap.ts --network base
yarn hardhat run scripts/timelockUpdateParaswap.ts --network base

# Wait 2 days, then execute
EXECUTE=true TIMELOCK_ADDRESS=0x... PROTOCOL_REGISTRY_ADDRESS=0x... NEW_PARASWAP_ADDRESS=0x... \
yarn hardhat run scripts/timelock-update-paraswap.ts --network base
EXECUTE=true OPERATION_ID="..." TIMELOCK_ADDRESS=0x... PROTOCOL_REGISTRY_ADDRESS=0x... NEW_PARASWAP_ADDRESS=0x... \
yarn hardhat run scripts/timelockUpdateParaswap.ts --network base
```

> **Note**: Updating the operator address (`setOperator`) also requires the timelock. Create a similar script based on `timelock-update-paraswap.ts` if needed.
**Updating the operator address:**

```bash
# Schedule operation (requires proposer role)
TIMELOCK_ADDRESS=0x... PROTOCOL_REGISTRY_ADDRESS=0x... NEW_OPERATOR_ADDRESS=0x... \
yarn hardhat run scripts/timelockUpdateOperator.ts --network base

# Wait 2 days, then execute (reuse the OPERATION_ID printed during scheduling)
EXECUTE=true OPERATION_ID="..." TIMELOCK_ADDRESS=0x... PROTOCOL_REGISTRY_ADDRESS=0x... NEW_OPERATOR_ADDRESS=0x... \
yarn hardhat run scripts/timelockUpdateOperator.ts --network base
```


## Security Features
Expand Down
Loading
Loading