Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions .github/actions/docker-image-comment/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ runs:
declare -A SERVICE_EMOJI=(
["rebalancer"]="♻️"
["warp-monitor"]="🕵️"
["key-funder"]="🔑"
["offchain-lookup-server"]="🔍"
["monorepo"]="📦"
)
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/node-services-docker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ on:
- 'typescript/rebalancer/**'
- 'typescript/warp-monitor/**'
- 'typescript/ccip-server/**'
- 'typescript/keyfunder/**'
- 'typescript/Dockerfile.node-service'
- 'pnpm-lock.yaml'
- '.github/workflows/node-services-docker.yml'
Expand Down Expand Up @@ -125,6 +126,7 @@ jobs:
TAGS=$(cat << EOF
${REGISTRY}/hyperlane-rebalancer:${TAG_SHA_DATE}
${REGISTRY}/hyperlane-warp-monitor:${TAG_SHA_DATE}
${REGISTRY}/hyperlane-key-funder:${TAG_SHA_DATE}
${REGISTRY}/hyperlane-offchain-lookup-server:${TAG_SHA_DATE}
EOF
)
Expand All @@ -140,6 +142,7 @@ jobs:
|---------|-----|
| ♻️ rebalancer | \`${TAG_SHA_DATE}\` |
| 🕵️ warp-monitor | \`${TAG_SHA_DATE}\` |
| 🔑 key-funder | \`${TAG_SHA_DATE}\` |
| 🔍 offchain-lookup-server | \`${TAG_SHA_DATE}\` |

**Full image paths:**
Expand Down
1 change: 1 addition & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ COPY typescript/github-proxy/package.json ./typescript/github-proxy/
COPY typescript/helloworld/package.json ./typescript/helloworld/
COPY typescript/http-registry-server/package.json ./typescript/http-registry-server/
COPY typescript/infra/package.json ./typescript/infra/
COPY typescript/keyfunder/package.json ./typescript/keyfunder/
COPY typescript/provider-sdk/package.json ./typescript/provider-sdk/
COPY typescript/radix-sdk/package.json ./typescript/radix-sdk/
COPY typescript/rebalancer/package.json ./typescript/rebalancer/
Expand Down
88 changes: 88 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions typescript/docker-bake.hcl
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ target "ncc-services" {
{ name = "rebalancer", dir = "rebalancer", package = "@hyperlane-xyz/rebalancer", image = "hyperlane-rebalancer", port = "" },
{ name = "warp-monitor", dir = "warp-monitor", package = "@hyperlane-xyz/warp-monitor", image = "hyperlane-warp-monitor", port = "" },
{ name = "ccip-server", dir = "ccip-server", package = "@hyperlane-xyz/ccip-server", image = "hyperlane-offchain-lookup-server", port = "3000" },
{ name = "keyfunder", dir = "keyfunder", package = "@hyperlane-xyz/keyfunder", image = "hyperlane-key-funder", port=""},
]
}

Expand Down
7 changes: 7 additions & 0 deletions typescript/keyfunder/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.env*
/dist
/bundle
/cache

# allow check-in of .env.example
!.env.example
3 changes: 3 additions & 0 deletions typescript/keyfunder/.mocharc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"import": ["tsx"]
}
178 changes: 178 additions & 0 deletions typescript/keyfunder/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
# @hyperlane-xyz/keyfunder

Standalone service for funding Hyperlane agent keys with native tokens across multiple chains.

## Overview

The KeyFunder service:

- Funds agent keys (relayers, kathy, rebalancer) to maintain desired balances
- Claims accumulated fees from InterchainGasPaymaster (IGP) contracts
- Sweeps excess funds from the funder wallet to a safe address

## Configuration

The service reads configuration from a YAML file. The file path is specified via the `KEYFUNDER_CONFIG_FILE` environment variable.

### Example Configuration

```yaml
version: '1'

# Roles define WHO gets funded (address defined once, reused across chains)
roles:
hyperlane-relayer:
address: '0x74cae0ecc47b02ed9b9d32e000fd70b9417970c5'
hyperlane-kathy:
address: '0x5fb02f40f56d15f0442a39d11a23f73747095b20'
hyperlane-rebalancer:
address: '0xdef456...'

# Chains define HOW MUCH each role gets (balances reference role names)
chains:
ethereum:
balances:
hyperlane-relayer: '0.5'
hyperlane-kathy: '0.4'
igp:
address: '0x6cA0B6D43F8e45C82e57eC5a5F2Bce4bF2b6F1f7'
claimThreshold: '0.2'
sweep:
enabled: true
address: '0x478be6076f31E9666123B9721D0B6631baD944AF'
threshold: '0.3'
targetMultiplier: 1.5
triggerMultiplier: 2.0
arbitrum:
balances:
hyperlane-relayer: '0.1'
igp:
address: '0x3b6044acd6767f017e99318AA6Ef93b7B06A5a22'
claimThreshold: '0.1'

metrics:
jobName: 'keyfunder-mainnet3'
labels:
environment: 'mainnet3'
chainsToSkip: []
```

### Configuration Options

| Field | Description |
| ---------------------------------------- | ------------------------------------------------------------------------------------------------ |
| `version` | Config version, must be "1" |
| `roles` | Role definitions (address per role) |
| `roles.<role>.address` | Ethereum address for this role |
| `chains` | Per-chain configuration |
| `chains.<chain>.balances` | Map of role name to desired balance |
| `chains.<chain>.balances.<role>` | Target balance decimal string (e.g., "0.5" ETH; up to 18 decimals) |
| `chains.<chain>.igp` | IGP claim configuration |
| `chains.<chain>.igp.address` | IGP contract address (required when `igp` is specified) |
| `chains.<chain>.igp.claimThreshold` | Minimum IGP balance before claiming (decimal string; up to 18 decimals) |
| `chains.<chain>.sweep` | Sweep excess funds configuration |
| `chains.<chain>.sweep.enabled` | Enable sweep functionality |
| `chains.<chain>.sweep.address` | Address to sweep funds to (required when enabled) |
| `chains.<chain>.sweep.threshold` | Base threshold for sweep calculations (required when enabled; decimal string; up to 18 decimals) |
| `chains.<chain>.sweep.targetMultiplier` | Multiplier for target balance (default: 1.5; 2 decimal precision, floored) |
| `chains.<chain>.sweep.triggerMultiplier` | Multiplier for trigger threshold (default: 2.0; 2 decimal precision, floored) |
| `metrics.jobName` | Job name for metrics |
| `metrics.labels` | Additional labels for metrics |
| `chainsToSkip` | Array of chain names to skip |

### Precision Notes

- **Balance strings**: Support up to 18 decimal places (standard ETH precision). Must include leading digit (e.g., `"0.5"` not `".5"`).
- **Multipliers**: Calculated with 2 decimal precision using floor (e.g., `1.555` is treated as `1.55`, not `1.56`).

## Environment Variables

| Variable | Description | Required |
| ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | -------- |
| `KEYFUNDER_CONFIG_FILE` | Path to config YAML file | Yes |
| `HYP_KEY` | Private key for funding wallet | Yes |
| `RPC_URL_<CHAIN>` | RPC URL per chain (e.g., `RPC_URL_ETHEREUM`, `RPC_URL_ARBITRUM`). Falls back to registry defaults if not set. | No |
| `REGISTRY_URI` | Hyperlane registry URI (default: GitHub registry). Supports commit pinning (e.g., `github://hyperlane-xyz/hyperlane-registry/commit/abc123`) | No |
| `SKIP_IGP_CLAIM` | Set to "true" to skip IGP claims | No |
| `PROMETHEUS_PUSH_GATEWAY` | Prometheus push gateway URL (e.g., `http://prometheus-pushgateway:9091`) | No |
| `SERVICE_VERSION` | Version identifier for logging (default: "dev") | No |
| `LOG_LEVEL` | Log level: DEBUG, INFO, WARN, ERROR | No |
| `LOG_FORMAT` | Log format: JSON, PRETTY | No |

In Kubernetes deployments, `HYP_KEY` and `RPC_URL_*` are injected via ExternalSecrets from GCP Secret Manager.

## Usage

### Docker

```bash
docker run -v /path/to/config.yaml:/config/keyfunder.yaml \
-e KEYFUNDER_CONFIG_FILE=/config/keyfunder.yaml \
-e HYP_KEY=0x... \
-e RPC_URL_ETHEREUM=https://... \
gcr.io/abacus-labs-dev/hyperlane-key-funder:latest
```

### Local Development

```bash
# Build
pnpm build

# Run locally
KEYFUNDER_CONFIG_FILE=./config.yaml HYP_KEY=0x... RPC_URL_ETHEREUM=https://... pnpm start:dev
```

### Bundle

The service can be bundled into a single file using ncc:

```bash
pnpm bundle
# Output: ./bundle/index.js
```

## Funding Logic

### Key Funding

Keys are funded when their balance drops below 40% of the desired balance. The funding amount brings the balance up to the full desired balance.

**Example**: If `desiredBalance` is `1.0 ETH` and current balance is `0.39 ETH` (39%), funding is triggered. The key receives `0.61 ETH` to reach the full `1.0 ETH`.

### IGP Claims

When the IGP contract balance exceeds the claim threshold, accumulated fees are claimed to the funder wallet.

### Sweep

When the funder wallet balance exceeds `threshold * triggerMultiplier`, excess funds are swept to the safe address, leaving `threshold * targetMultiplier` in the wallet.

**Example**: With `threshold: '1.0'`, `triggerMultiplier: 2.0`, `targetMultiplier: 1.5`:

- If funder balance > 2.0 ETH, sweep is triggered
- After sweep, funder balance = 1.5 ETH

### Timeouts

Each chain is processed with a 60-second timeout. If funding operations for a chain exceed this limit, the chain is marked as failed and processing continues with remaining chains.

## Metrics

The service exposes Prometheus metrics:

| Metric | Description |
| ------------------------------------------------ | ---------------------------- |
| `hyperlane_keyfunder_wallet_balance` | Current wallet balance |
| `hyperlane_keyfunder_funding_amount` | Amount funded to a key |
| `hyperlane_keyfunder_igp_balance` | IGP contract balance |
| `hyperlane_keyfunder_sweep_amount` | Amount swept to safe address |
| `hyperlane_keyfunder_operation_duration_seconds` | Duration of operations |

## Deployment

The service is typically deployed as a Kubernetes CronJob. See `typescript/infra/helm/key-funder/` for the Helm chart.

## License

Apache-2.0
3 changes: 3 additions & 0 deletions typescript/keyfunder/eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { jsRules, typescriptRules } from '@hyperlane-xyz/eslint-config';

export default [...jsRules, ...typescriptRules];
Loading
Loading