|
| 1 | +# @hyperlane-xyz/keyfunder |
| 2 | + |
| 3 | +Standalone service for funding Hyperlane agent keys with native tokens across multiple chains. |
| 4 | + |
| 5 | +## Overview |
| 6 | + |
| 7 | +The KeyFunder service: |
| 8 | + |
| 9 | +- Funds agent keys (relayers, kathy, rebalancer) to maintain desired balances |
| 10 | +- Claims accumulated fees from InterchainGasPaymaster (IGP) contracts |
| 11 | +- Sweeps excess funds from the funder wallet to a safe address |
| 12 | + |
| 13 | +## Configuration |
| 14 | + |
| 15 | +The service reads configuration from a YAML file. The file path is specified via the `KEYFUNDER_CONFIG_FILE` environment variable. |
| 16 | + |
| 17 | +### Example Configuration |
| 18 | + |
| 19 | +```yaml |
| 20 | +version: '1' |
| 21 | + |
| 22 | +# Roles define WHO gets funded (address defined once, reused across chains) |
| 23 | +roles: |
| 24 | + hyperlane-relayer: |
| 25 | + address: '0x74cae0ecc47b02ed9b9d32e000fd70b9417970c5' |
| 26 | + hyperlane-kathy: |
| 27 | + address: '0x5fb02f40f56d15f0442a39d11a23f73747095b20' |
| 28 | + hyperlane-rebalancer: |
| 29 | + address: '0xdef456...' |
| 30 | + |
| 31 | +# Chains define HOW MUCH each role gets (balances reference role names) |
| 32 | +chains: |
| 33 | + ethereum: |
| 34 | + balances: |
| 35 | + hyperlane-relayer: '0.5' |
| 36 | + hyperlane-kathy: '0.4' |
| 37 | + igp: |
| 38 | + address: '0x6cA0B6D43F8e45C82e57eC5a5F2Bce4bF2b6F1f7' |
| 39 | + claimThreshold: '0.2' |
| 40 | + sweep: |
| 41 | + enabled: true |
| 42 | + address: '0x478be6076f31E9666123B9721D0B6631baD944AF' |
| 43 | + threshold: '0.3' |
| 44 | + targetMultiplier: 1.5 |
| 45 | + triggerMultiplier: 2.0 |
| 46 | + arbitrum: |
| 47 | + balances: |
| 48 | + hyperlane-relayer: '0.1' |
| 49 | + igp: |
| 50 | + address: '0x3b6044acd6767f017e99318AA6Ef93b7B06A5a22' |
| 51 | + claimThreshold: '0.1' |
| 52 | + |
| 53 | +metrics: |
| 54 | + jobName: 'keyfunder-mainnet3' |
| 55 | + labels: |
| 56 | + environment: 'mainnet3' |
| 57 | +chainsToSkip: [] |
| 58 | +``` |
| 59 | +
|
| 60 | +### Configuration Options |
| 61 | +
|
| 62 | +| Field | Description | |
| 63 | +| ---------------------------------------- | ------------------------------------------------------------------------------------------------ | |
| 64 | +| `version` | Config version, must be "1" | |
| 65 | +| `roles` | Role definitions (address per role) | |
| 66 | +| `roles.<role>.address` | Ethereum address for this role | |
| 67 | +| `chains` | Per-chain configuration | |
| 68 | +| `chains.<chain>.balances` | Map of role name to desired balance | |
| 69 | +| `chains.<chain>.balances.<role>` | Target balance decimal string (e.g., "0.5" ETH; up to 18 decimals) | |
| 70 | +| `chains.<chain>.igp` | IGP claim configuration | |
| 71 | +| `chains.<chain>.igp.address` | IGP contract address (required when `igp` is specified) | |
| 72 | +| `chains.<chain>.igp.claimThreshold` | Minimum IGP balance before claiming (decimal string; up to 18 decimals) | |
| 73 | +| `chains.<chain>.sweep` | Sweep excess funds configuration | |
| 74 | +| `chains.<chain>.sweep.enabled` | Enable sweep functionality | |
| 75 | +| `chains.<chain>.sweep.address` | Address to sweep funds to (required when enabled) | |
| 76 | +| `chains.<chain>.sweep.threshold` | Base threshold for sweep calculations (required when enabled; decimal string; up to 18 decimals) | |
| 77 | +| `chains.<chain>.sweep.targetMultiplier` | Multiplier for target balance (default: 1.5; 2 decimal precision, floored) | |
| 78 | +| `chains.<chain>.sweep.triggerMultiplier` | Multiplier for trigger threshold (default: 2.0; 2 decimal precision, floored) | |
| 79 | +| `metrics.jobName` | Job name for metrics | |
| 80 | +| `metrics.labels` | Additional labels for metrics | |
| 81 | +| `chainsToSkip` | Array of chain names to skip | |
| 82 | + |
| 83 | +### Precision Notes |
| 84 | + |
| 85 | +- **Balance strings**: Support up to 18 decimal places (standard ETH precision). Must include leading digit (e.g., `"0.5"` not `".5"`). |
| 86 | +- **Multipliers**: Calculated with 2 decimal precision using floor (e.g., `1.555` is treated as `1.55`, not `1.56`). |
| 87 | + |
| 88 | +## Environment Variables |
| 89 | + |
| 90 | +| Variable | Description | Required | |
| 91 | +| ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | -------- | |
| 92 | +| `KEYFUNDER_CONFIG_FILE` | Path to config YAML file | Yes | |
| 93 | +| `HYP_KEY` | Private key for funding wallet | Yes | |
| 94 | +| `RPC_URL_<CHAIN>` | RPC URL per chain (e.g., `RPC_URL_ETHEREUM`, `RPC_URL_ARBITRUM`). Falls back to registry defaults if not set. | No | |
| 95 | +| `REGISTRY_URI` | Hyperlane registry URI (default: GitHub registry). Supports commit pinning (e.g., `github://hyperlane-xyz/hyperlane-registry/commit/abc123`) | No | |
| 96 | +| `SKIP_IGP_CLAIM` | Set to "true" to skip IGP claims | No | |
| 97 | +| `PROMETHEUS_PUSH_GATEWAY` | Prometheus push gateway URL (e.g., `http://prometheus-pushgateway:9091`) | No | |
| 98 | +| `SERVICE_VERSION` | Version identifier for logging (default: "dev") | No | |
| 99 | +| `LOG_LEVEL` | Log level: DEBUG, INFO, WARN, ERROR | No | |
| 100 | +| `LOG_FORMAT` | Log format: JSON, PRETTY | No | |
| 101 | + |
| 102 | +In Kubernetes deployments, `HYP_KEY` and `RPC_URL_*` are injected via ExternalSecrets from GCP Secret Manager. |
| 103 | + |
| 104 | +## Usage |
| 105 | + |
| 106 | +### Docker |
| 107 | + |
| 108 | +```bash |
| 109 | +docker run -v /path/to/config.yaml:/config/keyfunder.yaml \ |
| 110 | + -e KEYFUNDER_CONFIG_FILE=/config/keyfunder.yaml \ |
| 111 | + -e HYP_KEY=0x... \ |
| 112 | + -e RPC_URL_ETHEREUM=https://... \ |
| 113 | + gcr.io/abacus-labs-dev/hyperlane-key-funder:latest |
| 114 | +``` |
| 115 | + |
| 116 | +### Local Development |
| 117 | + |
| 118 | +```bash |
| 119 | +# Build |
| 120 | +pnpm build |
| 121 | +
|
| 122 | +# Run locally |
| 123 | +KEYFUNDER_CONFIG_FILE=./config.yaml HYP_KEY=0x... RPC_URL_ETHEREUM=https://... pnpm start:dev |
| 124 | +``` |
| 125 | + |
| 126 | +### Bundle |
| 127 | + |
| 128 | +The service can be bundled into a single file using ncc: |
| 129 | + |
| 130 | +```bash |
| 131 | +pnpm bundle |
| 132 | +# Output: ./bundle/index.js |
| 133 | +``` |
| 134 | + |
| 135 | +## Funding Logic |
| 136 | + |
| 137 | +### Key Funding |
| 138 | + |
| 139 | +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. |
| 140 | + |
| 141 | +**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`. |
| 142 | + |
| 143 | +### IGP Claims |
| 144 | + |
| 145 | +When the IGP contract balance exceeds the claim threshold, accumulated fees are claimed to the funder wallet. |
| 146 | + |
| 147 | +### Sweep |
| 148 | + |
| 149 | +When the funder wallet balance exceeds `threshold * triggerMultiplier`, excess funds are swept to the safe address, leaving `threshold * targetMultiplier` in the wallet. |
| 150 | + |
| 151 | +**Example**: With `threshold: '1.0'`, `triggerMultiplier: 2.0`, `targetMultiplier: 1.5`: |
| 152 | + |
| 153 | +- If funder balance > 2.0 ETH, sweep is triggered |
| 154 | +- After sweep, funder balance = 1.5 ETH |
| 155 | + |
| 156 | +### Timeouts |
| 157 | + |
| 158 | +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. |
| 159 | + |
| 160 | +## Metrics |
| 161 | + |
| 162 | +The service exposes Prometheus metrics: |
| 163 | + |
| 164 | +| Metric | Description | |
| 165 | +| ------------------------------------------------ | ---------------------------- | |
| 166 | +| `hyperlane_keyfunder_wallet_balance` | Current wallet balance | |
| 167 | +| `hyperlane_keyfunder_funding_amount` | Amount funded to a key | |
| 168 | +| `hyperlane_keyfunder_igp_balance` | IGP contract balance | |
| 169 | +| `hyperlane_keyfunder_sweep_amount` | Amount swept to safe address | |
| 170 | +| `hyperlane_keyfunder_operation_duration_seconds` | Duration of operations | |
| 171 | + |
| 172 | +## Deployment |
| 173 | + |
| 174 | +The service is typically deployed as a Kubernetes CronJob. See `typescript/infra/helm/key-funder/` for the Helm chart. |
| 175 | + |
| 176 | +## License |
| 177 | + |
| 178 | +Apache-2.0 |
0 commit comments